From d81a3459e391b63968ddbf8282fb9cb703067677 Mon Sep 17 00:00:00 2001 From: Mourad Maatoug Date: Sun, 12 Apr 2026 17:42:06 +0200 Subject: [PATCH 001/556] fix(ui): resolve config page layout regression caused by flex on main (#1851) --- warpgate-web/src/admin/App.svelte | 2 -- warpgate-web/src/admin/Log.svelte | 59 ++++++++++++++++++------------- 2 files changed, 35 insertions(+), 26 deletions(-) diff --git a/warpgate-web/src/admin/App.svelte b/warpgate-web/src/admin/App.svelte index ca138dcad..6296b22a5 100644 --- a/warpgate-web/src/admin/App.svelte +++ b/warpgate-web/src/admin/App.svelte @@ -101,8 +101,6 @@ main { flex: 1 0 0; - display: flex; - flex-direction: column; } header { diff --git a/warpgate-web/src/admin/Log.svelte b/warpgate-web/src/admin/Log.svelte index 987a024cb..aa1c725f6 100644 --- a/warpgate-web/src/admin/Log.svelte +++ b/warpgate-web/src/admin/Log.svelte @@ -29,31 +29,42 @@ let filters = $derived({ }) -
-

- {#if filterKind === 'user'} - user audit log: UID {params?.id} - {:else if filterKind === 'access-role'} - access role audit log: ID {params?.id} - {:else if filterKind === 'admin-role'} - admin role audit log: ID {params?.id} - {:else} - log - {/if} -

-
- {#if !filterKind} - +
+
+

+ {#if filterKind === 'user'} + user audit log: UID {params?.id} + {:else if filterKind === 'access-role'} + access role audit log: ID {params?.id} + {:else if filterKind === 'admin-role'} + admin role audit log: ID {params?.id} + {:else} + log {/if} +

+
+ {#if !filterKind} + + {/if} +
+ + {#key `${$target}-${filterKind}-${params?.id}`} + + {/key}
-{#key `${$target}-${filterKind}-${params?.id}`} - -{/key} + From 485876dcaa7cc8c6f26f50f3e4cdebab65a6d335 Mon Sep 17 00:00:00 2001 From: Eugene Date: Sun, 12 Apr 2026 23:47:38 +0200 Subject: [PATCH 002/556] streamline x-forwarded header checks (#1858) --- warpgate-admin/src/api/admin_roles.rs | 12 +-- .../src/api/certificate_credentials.rs | 8 +- warpgate-admin/src/api/common.rs | 2 +- warpgate-admin/src/api/known_hosts_detail.rs | 2 +- warpgate-admin/src/api/known_hosts_list.rs | 4 +- warpgate-admin/src/api/ldap_servers.rs | 14 +-- warpgate-admin/src/api/logs.rs | 2 +- warpgate-admin/src/api/otp_credentials.rs | 6 +- warpgate-admin/src/api/parameters.rs | 4 +- .../src/api/password_credentials.rs | 6 +- .../src/api/public_key_credentials.rs | 8 +- warpgate-admin/src/api/recordings_detail.rs | 16 ++-- warpgate-admin/src/api/roles.rs | 14 +-- warpgate-admin/src/api/sessions_detail.rs | 6 +- warpgate-admin/src/api/sessions_list.rs | 7 +- warpgate-admin/src/api/ssh_connection_test.rs | 2 +- warpgate-admin/src/api/ssh_keys.rs | 4 +- warpgate-admin/src/api/sso_credentials.rs | 8 +- warpgate-admin/src/api/target_groups.rs | 10 +-- warpgate-admin/src/api/targets.rs | 20 ++--- warpgate-admin/src/api/tickets_detail.rs | 2 +- warpgate-admin/src/api/tickets_list.rs | 4 +- warpgate-admin/src/api/users.rs | 36 ++++---- warpgate-common-http/src/auth.rs | 87 ++++++++++++++++++- warpgate-core/src/config_providers/db.rs | 2 +- .../src/UserRoleAssignment.rs | 8 +- warpgate-protocol-http/src/api/api_tokens.rs | 6 +- warpgate-protocol-http/src/api/auth.rs | 20 ++--- warpgate-protocol-http/src/api/credentials.rs | 18 ++-- warpgate-protocol-http/src/api/info.rs | 10 +-- .../src/api/sso_provider_detail.rs | 4 +- .../src/api/sso_provider_list.rs | 22 ++++- .../src/api/targets_list.rs | 2 +- warpgate-protocol-http/src/catchall.rs | 14 ++- warpgate-protocol-http/src/common.rs | 14 ++- warpgate-protocol-http/src/lib.rs | 8 +- .../src/middleware/cookie_host.rs | 21 ++--- .../src/middleware/ticket.rs | 4 +- warpgate-protocol-http/src/proxy.rs | 28 +++--- warpgate-protocol-http/src/session.rs | 2 +- .../src/server/handlers.rs | 11 +-- .../src/server/mod.rs | 4 +- warpgate/src/commands/create_user.rs | 3 +- 43 files changed, 281 insertions(+), 204 deletions(-) diff --git a/warpgate-admin/src/api/admin_roles.rs b/warpgate-admin/src/api/admin_roles.rs index ea92fe51d..1b889397f 100644 --- a/warpgate-admin/src/api/admin_roles.rs +++ b/warpgate-admin/src/api/admin_roles.rs @@ -108,7 +108,7 @@ impl ListApi { ) -> Result { require_admin_permission(&ctx, None).await?; - let db = ctx.services.db.lock().await; + let db = ctx.services().db.lock().await; let mut roles = AdminRole::Entity::find().order_by_asc(AdminRole::Column::Name); if let Some(ref search) = *search { @@ -135,7 +135,7 @@ impl ListApi { ) -> Result { require_admin_permission(&ctx, Some(AdminPermission::AdminRolesManage)).await?; - let db = ctx.services.db.lock().await; + let db = ctx.services().db.lock().await; let values = AdminRole::ActiveModel { id: Set(Uuid::new_v4()), name: Set(body.name.clone()), @@ -182,7 +182,7 @@ impl DetailApi { ) -> Result { require_admin_permission(&ctx, None).await?; - let db = ctx.services.db.lock().await; + let db = ctx.services().db.lock().await; let role = AdminRole::Entity::find_by_id(id.0).one(&*db).await?; Ok(match role { Some(r) => GetAdminRoleResponse::Ok(Json(r.into())), @@ -204,7 +204,7 @@ impl DetailApi { ) -> Result { require_admin_permission(&ctx, Some(AdminPermission::AdminRolesManage)).await?; - let db = ctx.services.db.lock().await; + let db = ctx.services().db.lock().await; let Some(role) = AdminRole::Entity::find_by_id(id.0).one(&*db).await? else { return Ok(UpdateAdminRoleResponse::NotFound); }; @@ -246,7 +246,7 @@ impl DetailApi { ) -> Result { require_admin_permission(&ctx, Some(AdminPermission::AdminRolesManage)).await?; - let db = ctx.services.db.lock().await; + let db = ctx.services().db.lock().await; let Some(role) = AdminRole::Entity::find_by_id(id.0).one(&*db).await? else { return Ok(DeleteAdminRoleResponse::NotFound); }; @@ -273,7 +273,7 @@ impl DetailApi { ) -> Result { require_admin_permission(&ctx, None).await?; - let db = ctx.services.db.lock().await; + let db = ctx.services().db.lock().await; let Some((_, users)) = AdminRole::Entity::find_by_id(id.0) .find_with_related(User::Entity) .all(&*db) diff --git a/warpgate-admin/src/api/certificate_credentials.rs b/warpgate-admin/src/api/certificate_credentials.rs index 6327fe8d6..56a268e26 100644 --- a/warpgate-admin/src/api/certificate_credentials.rs +++ b/warpgate-admin/src/api/certificate_credentials.rs @@ -100,7 +100,7 @@ impl ListApi { ) -> Result { require_admin_permission(&ctx, Some(AdminPermission::UsersEdit)).await?; - let db = ctx.services.db.lock().await; + let db = ctx.services().db.lock().await; let objects = CertificateCredential::Entity::find() .filter(CertificateCredential::Column::UserId.eq(*user_id)) @@ -126,7 +126,7 @@ impl ListApi { ) -> Result { require_admin_permission(&ctx, Some(AdminPermission::UsersEdit)).await?; - let db = ctx.services.db.lock().await; + let db = ctx.services().db.lock().await; let params = Parameters::Entity::get(&db).await?; let ca = warpgate_ca::deserialize_ca(¶ms.ca_certificate_pem, ¶ms.ca_private_key_pem)?; @@ -199,7 +199,7 @@ impl DetailApi { ) -> Result { require_admin_permission(&ctx, Some(AdminPermission::UsersEdit)).await?; - let db = ctx.services.db.lock().await; + let db = ctx.services().db.lock().await; let Some(cred) = CertificateCredential::Entity::find_by_id(id.0) .filter(CertificateCredential::Column::UserId.eq(*user_id)) .one(&*db) @@ -232,7 +232,7 @@ impl DetailApi { ) -> Result { require_admin_permission(&ctx, Some(AdminPermission::UsersEdit)).await?; - let db = ctx.services.db.lock().await; + let db = ctx.services().db.lock().await; let Some(model) = CertificateCredential::Entity::find_by_id(id.0) .filter(CertificateCredential::Column::UserId.eq(*user_id)) diff --git a/warpgate-admin/src/api/common.rs b/warpgate-admin/src/api/common.rs index a7d997749..70f06da79 100644 --- a/warpgate-admin/src/api/common.rs +++ b/warpgate-admin/src/api/common.rs @@ -24,7 +24,7 @@ pub async fn has_admin_permission( RequestAuthorization::AdminToken => unreachable!(), }; - let db = ctx.services.db.lock().await; + let db = ctx.services().db.lock().await; let Some(user_model) = User::Entity::find() .filter(User::Column::Username.eq(username)) diff --git a/warpgate-admin/src/api/known_hosts_detail.rs b/warpgate-admin/src/api/known_hosts_detail.rs index c48215c7a..5eded3a86 100644 --- a/warpgate-admin/src/api/known_hosts_detail.rs +++ b/warpgate-admin/src/api/known_hosts_detail.rs @@ -35,7 +35,7 @@ impl Api { ) -> Result { require_admin_permission(&ctx, Some(AdminPermission::ConfigEdit)).await?; - let db = ctx.services.db.lock().await; + let db = ctx.services().db.lock().await; let known_host = KnownHost::Entity::find_by_id(id.0).one(&*db).await?; diff --git a/warpgate-admin/src/api/known_hosts_list.rs b/warpgate-admin/src/api/known_hosts_list.rs index fc158d01e..f6ab712fe 100644 --- a/warpgate-admin/src/api/known_hosts_list.rs +++ b/warpgate-admin/src/api/known_hosts_list.rs @@ -56,7 +56,7 @@ impl Api { PublicKey::from_openssh(&format!("{} {}", body.key_type, body.key_base64)) .context("parsing key")?; - let db = ctx.services.db.lock().await; + let db = ctx.services().db.lock().await; let model = KnownHost::ActiveModel { id: Set(Uuid::new_v4()), host: Set(body.host.clone()), @@ -81,7 +81,7 @@ impl Api { ) -> Result { require_admin_permission(&ctx, Some(AdminPermission::ConfigEdit)).await?; - let db = ctx.services.db.lock().await; + let db = ctx.services().db.lock().await; let hosts = KnownHost::Entity::find().all(&*db).await?; Ok(GetSSHKnownHostsResponse::Ok(Json(hosts))) } diff --git a/warpgate-admin/src/api/ldap_servers.rs b/warpgate-admin/src/api/ldap_servers.rs index a05e1582b..34542c3fa 100644 --- a/warpgate-admin/src/api/ldap_servers.rs +++ b/warpgate-admin/src/api/ldap_servers.rs @@ -53,7 +53,7 @@ impl ImportApi { return Ok(ImportLdapUsersResponse::Ok(Json(vec![]))); } - let db = ctx.services.db.lock().await; + let db = ctx.services().db.lock().await; let Some(server) = LdapServer::Entity::find_by_id(id.0).one(&*db).await? else { return Ok(ImportLdapUsersResponse::NotFound); }; @@ -295,7 +295,7 @@ impl ListApi { ) -> Result { require_admin_permission(&ctx, Some(AdminPermission::ConfigEdit)).await?; - let db = ctx.services.db.lock().await; + let db = ctx.services().db.lock().await; let mut query = LdapServer::Entity::find().order_by_asc(LdapServer::Column::Name); @@ -330,7 +330,7 @@ impl ListApi { ))); } - let db = ctx.services.db.lock().await; + let db = ctx.services().db.lock().await; // Check if name already exists let existing = LdapServer::Entity::find() @@ -512,7 +512,7 @@ impl DetailApi { ) -> Result { require_admin_permission(&ctx, Some(AdminPermission::ConfigEdit)).await?; - let db = ctx.services.db.lock().await; + let db = ctx.services().db.lock().await; let Some(server) = LdapServer::Entity::find_by_id(id.0).one(&*db).await? else { return Ok(GetLdapServerResponse::NotFound); @@ -535,7 +535,7 @@ impl DetailApi { ) -> Result { require_admin_permission(&ctx, Some(AdminPermission::ConfigEdit)).await?; - let db = ctx.services.db.lock().await; + let db = ctx.services().db.lock().await; let Some(server) = LdapServer::Entity::find_by_id(id.0).one(&*db).await? else { return Ok(UpdateLdapServerResponse::NotFound); @@ -601,7 +601,7 @@ impl DetailApi { ) -> Result { require_admin_permission(&ctx, Some(AdminPermission::ConfigEdit)).await?; - let db = ctx.services.db.lock().await; + let db = ctx.services().db.lock().await; let Some(server) = LdapServer::Entity::find_by_id(id.0).one(&*db).await? else { return Ok(DeleteLdapServerResponse::NotFound); @@ -642,7 +642,7 @@ impl QueryApi { ) -> Result { require_admin_permission(&ctx, Some(AdminPermission::UsersCreate)).await?; - let db = ctx.services.db.lock().await; + let db = ctx.services().db.lock().await; let Some(server) = LdapServer::Entity::find_by_id(id.0).one(&*db).await? else { return Ok(GetLdapUsersResponse::NotFound); diff --git a/warpgate-admin/src/api/logs.rs b/warpgate-admin/src/api/logs.rs index 1c4ee20bd..e01625fe1 100644 --- a/warpgate-admin/src/api/logs.rs +++ b/warpgate-admin/src/api/logs.rs @@ -46,7 +46,7 @@ impl Api { require_admin_permission(&ctx, None).await?; - let db = ctx.services.db.lock().await; + let db = ctx.services().db.lock().await; let mut q = LogEntry::Entity::find() .order_by_desc(LogEntry::Column::Timestamp) .limit(body.limit.unwrap_or(100)); diff --git a/warpgate-admin/src/api/otp_credentials.rs b/warpgate-admin/src/api/otp_credentials.rs index 04aa14541..051f1c5f8 100644 --- a/warpgate-admin/src/api/otp_credentials.rs +++ b/warpgate-admin/src/api/otp_credentials.rs @@ -67,7 +67,7 @@ impl ListApi { ) -> Result { require_admin_permission(&ctx, Some(AdminPermission::UsersEdit)).await?; - let db = ctx.services.db.lock().await; + let db = ctx.services().db.lock().await; let objects = OtpCredential::Entity::find() .filter(OtpCredential::Column::UserId.eq(*user_id)) @@ -93,7 +93,7 @@ impl ListApi { ) -> Result { require_admin_permission(&ctx, Some(AdminPermission::UsersEdit)).await?; - let db = ctx.services.db.lock().await; + let db = ctx.services().db.lock().await; let object = OtpCredential::ActiveModel { id: Set(Uuid::new_v4()), @@ -148,7 +148,7 @@ impl DetailApi { ) -> Result { require_admin_permission(&ctx, Some(AdminPermission::UsersEdit)).await?; - let db = ctx.services.db.lock().await; + let db = ctx.services().db.lock().await; let Some(role) = OtpCredential::Entity::find_by_id(id.0) .filter(OtpCredential::Column::UserId.eq(*user_id)) diff --git a/warpgate-admin/src/api/parameters.rs b/warpgate-admin/src/api/parameters.rs index 5b85de6a4..b53f1aa02 100644 --- a/warpgate-admin/src/api/parameters.rs +++ b/warpgate-admin/src/api/parameters.rs @@ -55,7 +55,7 @@ impl Api { ) -> Result { require_admin_permission(&ctx, None).await?; - let db = ctx.services.db.lock().await; + let db = ctx.services().db.lock().await; let parameters = Parameters::Entity::get(&db).await?; Ok(GetParametersResponse::Ok(Json(ParameterValues { @@ -81,7 +81,7 @@ impl Api { ) -> Result { require_admin_permission(&ctx, Some(AdminPermission::ConfigEdit)).await?; - let services = &ctx.services; + let services = ctx.services(); let db = services.db.lock().await; let mut parameters = Parameters::Entity::get(&db).await?.into_active_model(); diff --git a/warpgate-admin/src/api/password_credentials.rs b/warpgate-admin/src/api/password_credentials.rs index b3b871edd..15b325bae 100644 --- a/warpgate-admin/src/api/password_credentials.rs +++ b/warpgate-admin/src/api/password_credentials.rs @@ -59,7 +59,7 @@ impl ListApi { ) -> Result { require_admin_permission(&ctx, Some(AdminPermission::UsersEdit)).await?; - let db = ctx.services.db.lock().await; + let db = ctx.services().db.lock().await; let objects = PasswordCredential::Entity::find() .filter(PasswordCredential::Column::UserId.eq(*user_id)) @@ -85,7 +85,7 @@ impl ListApi { ) -> Result { require_admin_permission(&ctx, Some(AdminPermission::UsersEdit)).await?; - let db = ctx.services.db.lock().await; + let db = ctx.services().db.lock().await; let object = PasswordCredential::ActiveModel { id: Set(Uuid::new_v4()), @@ -144,7 +144,7 @@ impl DetailApi { ) -> Result { require_admin_permission(&ctx, Some(AdminPermission::UsersEdit)).await?; - let db = ctx.services.db.lock().await; + let db = ctx.services().db.lock().await; let Some(model) = PasswordCredential::Entity::find_by_id(id.0) .filter(PasswordCredential::Column::UserId.eq(*user_id)) diff --git a/warpgate-admin/src/api/public_key_credentials.rs b/warpgate-admin/src/api/public_key_credentials.rs index 661f07dcf..177d0d440 100644 --- a/warpgate-admin/src/api/public_key_credentials.rs +++ b/warpgate-admin/src/api/public_key_credentials.rs @@ -122,7 +122,7 @@ impl ListApi { ) -> Result { require_admin_permission(&ctx, Some(AdminPermission::UsersEdit)).await?; - let db = ctx.services.db.lock().await; + let db = ctx.services().db.lock().await; let objects = PublicKeyCredential::Entity::find() .filter(PublicKeyCredential::Column::UserId.eq(*user_id)) @@ -148,7 +148,7 @@ impl ListApi { ) -> Result { require_admin_permission(&ctx, Some(AdminPermission::UsersEdit)).await?; - let db = ctx.services.db.lock().await; + let db = ctx.services().db.lock().await; // Ensure user exists and is not LDAP-linked let Some(user) = User::Entity::find_by_id(*user_id).one(&*db).await? else { @@ -217,7 +217,7 @@ impl DetailApi { ) -> Result { require_admin_permission(&ctx, Some(AdminPermission::UsersEdit)).await?; - let db = ctx.services.db.lock().await; + let db = ctx.services().db.lock().await; // Ensure user exists and is not LDAP-linked let Some(_) = User::Entity::find_by_id(*user_id).one(&*db).await? else { @@ -261,7 +261,7 @@ impl DetailApi { ) -> Result { require_admin_permission(&ctx, Some(AdminPermission::UsersEdit)).await?; - let db = ctx.services.db.lock().await; + let db = ctx.services().db.lock().await; // Check if user is LDAP-linked if let Err(msg) = verify_user_not_ldap_linked(&db, *user_id).await { diff --git a/warpgate-admin/src/api/recordings_detail.rs b/warpgate-admin/src/api/recordings_detail.rs index 4cd903420..bf8f5a446 100644 --- a/warpgate-admin/src/api/recordings_detail.rs +++ b/warpgate-admin/src/api/recordings_detail.rs @@ -55,7 +55,7 @@ impl Api { ) -> poem::Result { require_admin_permission(&ctx, Some(AdminPermission::RecordingsView)).await?; - let db = ctx.services.db.lock().await; + let db = ctx.services().db.lock().await; let recording = Recording::Entity::find_by_id(id.0) .one(&*db) @@ -81,8 +81,8 @@ impl Api { ) -> poem::Result { require_admin_permission(&ctx, Some(AdminPermission::RecordingsView)).await?; - let db = ctx.services.db.lock().await; - let recordings = ctx.services.recordings.lock().await; + let db = ctx.services().db.lock().await; + let recordings = ctx.services().recordings.lock().await; let recording = Recording::Entity::find_by_id(id.0) .filter(Recording::Column::Kind.eq(RecordingKind::Kubernetes)) @@ -118,7 +118,7 @@ pub async fn api_get_recording_cast( ) -> poem::Result { require_admin_permission(&ctx, Some(AdminPermission::RecordingsView)).await?; - let db = ctx.services.db.lock().await; + let db = ctx.services().db.lock().await; let recording = Recording::Entity::find_by_id(id.0) .filter(Recording::Column::Kind.eq(RecordingKind::Terminal)) @@ -131,7 +131,7 @@ pub async fn api_get_recording_cast( }; let path = { - ctx.services + ctx.services() .recordings .lock() .await @@ -176,7 +176,7 @@ pub async fn api_get_recording_tcpdump( ) -> poem::Result { require_admin_permission(&ctx, Some(AdminPermission::RecordingsView)).await?; - let db = ctx.services.db.lock().await; + let db = ctx.services().db.lock().await; let recording = Recording::Entity::find_by_id(id.0) .filter(Recording::Column::Kind.eq(RecordingKind::Traffic)) @@ -189,7 +189,7 @@ pub async fn api_get_recording_tcpdump( }; let path = { - ctx.services + ctx.services() .recordings .lock() .await @@ -207,7 +207,7 @@ pub async fn api_get_recording_stream( ctx: Data<&AuthenticatedRequestContext>, id: poem::web::Path, ) -> impl IntoResponse { - let recordings = ctx.services.recordings.lock().await; + let recordings = ctx.services().recordings.lock().await; let receiver = recordings.subscribe_live(&id).await; ws.on_upgrade(|socket| async move { diff --git a/warpgate-admin/src/api/roles.rs b/warpgate-admin/src/api/roles.rs index 615665efd..ed78afeff 100644 --- a/warpgate-admin/src/api/roles.rs +++ b/warpgate-admin/src/api/roles.rs @@ -49,7 +49,7 @@ impl ListApi { require_admin_permission(&ctx, None).await?; // listing roles is allowed for any administrator - let db = ctx.services.db.lock().await; + let db = ctx.services().db.lock().await; let mut roles = Role::Entity::find().order_by_asc(Role::Column::Name); @@ -80,7 +80,7 @@ impl ListApi { return Ok(CreateRoleResponse::BadRequest(Json("name".into()))); } - let db = ctx.services.db.lock().await; + let db = ctx.services().db.lock().await; let values = Role::ActiveModel { id: Set(Uuid::new_v4()), @@ -147,7 +147,7 @@ impl DetailApi { ) -> Result { require_admin_permission(&ctx, None).await?; - let db = ctx.services.db.lock().await; + let db = ctx.services().db.lock().await; let role = Role::Entity::find_by_id(id.0).one(&*db).await?; @@ -167,7 +167,7 @@ impl DetailApi { ) -> Result { require_admin_permission(&ctx, Some(AdminPermission::AccessRolesEdit)).await?; - let db = ctx.services.db.lock().await; + let db = ctx.services().db.lock().await; let Some(role) = Role::Entity::find_by_id(id.0).one(&*db).await? else { return Ok(UpdateRoleResponse::NotFound); @@ -190,7 +190,7 @@ impl DetailApi { ) -> Result { require_admin_permission(&ctx, Some(AdminPermission::AccessRolesDelete)).await?; - let db = ctx.services.db.lock().await; + let db = ctx.services().db.lock().await; let Some(role) = Role::Entity::find_by_id(id.0).one(&*db).await? else { return Ok(DeleteRoleResponse::NotFound); @@ -224,7 +224,7 @@ impl DetailApi { ) -> Result { require_admin_permission(&ctx, None).await?; - let db = ctx.services.db.lock().await; + let db = ctx.services().db.lock().await; let Some(role) = Role::Entity::find_by_id(id.0).one(&*db).await? else { return Ok(GetRoleTargetsResponse::NotFound); @@ -253,7 +253,7 @@ impl DetailApi { ) -> Result { require_admin_permission(&ctx, None).await?; - let db = ctx.services.db.lock().await; + let db = ctx.services().db.lock().await; let Some(role) = Role::Entity::find_by_id(id.0).one(&*db).await? else { return Ok(GetRoleUsersResponse::NotFound); diff --git a/warpgate-admin/src/api/sessions_detail.rs b/warpgate-admin/src/api/sessions_detail.rs index 6cc7d6474..572075ca8 100644 --- a/warpgate-admin/src/api/sessions_detail.rs +++ b/warpgate-admin/src/api/sessions_detail.rs @@ -48,7 +48,7 @@ impl Api { ) -> Result { require_admin_permission(&ctx, Some(AdminPermission::SessionsView)).await?; - let db = ctx.services.db.lock().await; + let db = ctx.services().db.lock().await; let session = Session::Entity::find_by_id(id.0).one(&*db).await?; @@ -71,7 +71,7 @@ impl Api { ) -> Result { require_admin_permission(&ctx, Some(AdminPermission::RecordingsView)).await?; - let db = ctx.services.db.lock().await; + let db = ctx.services().db.lock().await; let recordings: Vec = Recording::Entity::find() .order_by_desc(Recording::Column::Started) .filter(Recording::Column::SessionId.eq(id.0)) @@ -93,7 +93,7 @@ impl Api { ) -> Result { require_admin_permission(&ctx, Some(AdminPermission::SessionsTerminate)).await?; - let state = ctx.services.state.lock().await; + let state = ctx.services().state.lock().await; if let Some(s) = state.sessions.get(&id) { let mut session = s.lock().await; diff --git a/warpgate-admin/src/api/sessions_list.rs b/warpgate-admin/src/api/sessions_list.rs index 2011dbf2c..ace97a545 100644 --- a/warpgate-admin/src/api/sessions_list.rs +++ b/warpgate-admin/src/api/sessions_list.rs @@ -31,6 +31,7 @@ enum CloseAllSessionsResponse { #[OpenApi] impl Api { + #[allow(clippy::too_many_arguments)] #[oai(path = "/sessions", method = "get", operation_id = "get_sessions")] async fn api_get_all_sessions( &self, @@ -46,7 +47,7 @@ impl Api { require_admin_permission(&ctx, Some(AdminPermission::SessionsView)).await?; - let db = ctx.services.db.lock().await; + let db = ctx.services().db.lock().await; let mut q = Session::Entity::find().order_by_desc(Session::Column::Started); if active_only.unwrap_or(false) { @@ -86,7 +87,7 @@ impl Api { ) -> poem::Result { require_admin_permission(&ctx, Some(AdminPermission::SessionsTerminate)).await?; - let state = ctx.services.state.lock().await; + let state = ctx.services().state.lock().await; for s in state.sessions.values() { let mut session = s.lock().await; @@ -106,7 +107,7 @@ pub async fn api_get_sessions_changes_stream( ) -> Result { require_admin_permission(&ctx, Some(AdminPermission::SessionsView)).await?; - let mut receiver = ctx.services.state.lock().await.subscribe(); + let mut receiver = ctx.services().state.lock().await.subscribe(); Ok(ws .on_upgrade(|socket| async move { diff --git a/warpgate-admin/src/api/ssh_connection_test.rs b/warpgate-admin/src/api/ssh_connection_test.rs index 8789fee6c..3906aec5d 100644 --- a/warpgate-admin/src/api/ssh_connection_test.rs +++ b/warpgate-admin/src/api/ssh_connection_test.rs @@ -49,7 +49,7 @@ impl Api { ) -> Result { require_admin_permission(&ctx, Some(AdminPermission::TargetsEdit)).await?; - let mut handles = RemoteClient::create(Uuid::new_v4(), ctx.services.clone())?; + let mut handles = RemoteClient::create(Uuid::new_v4(), ctx.services().clone())?; let _ = handles.command_tx.send(( RCCommand::Connect(TargetSSHOptions { diff --git a/warpgate-admin/src/api/ssh_keys.rs b/warpgate-admin/src/api/ssh_keys.rs index 2dc49411b..bd1c04670 100644 --- a/warpgate-admin/src/api/ssh_keys.rs +++ b/warpgate-admin/src/api/ssh_keys.rs @@ -37,9 +37,9 @@ impl Api { ) -> Result { require_admin_permission(&ctx, None).await?; - let config = ctx.services.config.lock().await; + let config = ctx.services().config.lock().await; let keys = - warpgate_protocol_ssh::load_keys(&config, &ctx.services.global_params, "client")?; + warpgate_protocol_ssh::load_keys(&config, &ctx.services().global_params, "client")?; let keys = keys .into_iter() diff --git a/warpgate-admin/src/api/sso_credentials.rs b/warpgate-admin/src/api/sso_credentials.rs index 36f0f87b3..39c4f9aaa 100644 --- a/warpgate-admin/src/api/sso_credentials.rs +++ b/warpgate-admin/src/api/sso_credentials.rs @@ -83,7 +83,7 @@ impl ListApi { ) -> Result { require_admin_permission(&ctx, Some(AdminPermission::UsersEdit)).await?; - let db = ctx.services.db.lock().await; + let db = ctx.services().db.lock().await; let objects = SsoCredential::Entity::find() .filter(SsoCredential::Column::UserId.eq(*user_id)) @@ -109,7 +109,7 @@ impl ListApi { ) -> Result { require_admin_permission(&ctx, Some(AdminPermission::UsersEdit)).await?; - let db = ctx.services.db.lock().await; + let db = ctx.services().db.lock().await; let object = SsoCredential::ActiveModel { id: Set(Uuid::new_v4()), @@ -166,7 +166,7 @@ impl DetailApi { ) -> Result { require_admin_permission(&ctx, Some(AdminPermission::UsersEdit)).await?; - let db = ctx.services.db.lock().await; + let db = ctx.services().db.lock().await; let model = SsoCredential::ActiveModel { id: Set(id.0), @@ -197,7 +197,7 @@ impl DetailApi { ) -> Result { require_admin_permission(&ctx, Some(AdminPermission::UsersEdit)).await?; - let db = ctx.services.db.lock().await; + let db = ctx.services().db.lock().await; let Some(role) = SsoCredential::Entity::find_by_id(id.0) .filter(SsoCredential::Column::UserId.eq(*user_id)) diff --git a/warpgate-admin/src/api/target_groups.rs b/warpgate-admin/src/api/target_groups.rs index 202ac4617..c2520b3c0 100644 --- a/warpgate-admin/src/api/target_groups.rs +++ b/warpgate-admin/src/api/target_groups.rs @@ -56,7 +56,7 @@ impl ListApi { ) -> Result { require_admin_permission(&ctx, None).await?; - let db = ctx.services.db.lock().await; + let db = ctx.services().db.lock().await; let groups = TargetGroup::Entity::find() .order_by_asc(TargetGroup::Column::Name) .all(&*db) @@ -82,7 +82,7 @@ impl ListApi { return Ok(CreateTargetGroupResponse::BadRequest(Json("name".into()))); } - let db = ctx.services.db.lock().await; + let db = ctx.services().db.lock().await; let existing = TargetGroup::Entity::find() .filter(TargetGroup::Column::Name.eq(body.name.clone())) .one(&*db) @@ -150,7 +150,7 @@ impl DetailApi { ) -> Result { require_admin_permission(&ctx, None).await?; - let db = ctx.services.db.lock().await; + let db = ctx.services().db.lock().await; let group = TargetGroup::Entity::find_by_id(id.0).one(&*db).await?; match group { @@ -177,7 +177,7 @@ impl DetailApi { return Ok(UpdateTargetGroupResponse::BadRequest); } - let db = ctx.services.db.lock().await; + let db = ctx.services().db.lock().await; let group = TargetGroup::Entity::find_by_id(id.0).one(&*db).await?; let Some(group) = group else { @@ -218,7 +218,7 @@ impl DetailApi { require_admin_permission(&ctx, Some(AdminPermission::TargetsDelete)).await?; - let db = ctx.services.db.lock().await; + let db = ctx.services().db.lock().await; let group = TargetGroup::Entity::find_by_id(id.0).one(&*db).await?; let Some(group) = group else { diff --git a/warpgate-admin/src/api/targets.rs b/warpgate-admin/src/api/targets.rs index a41e7fc4e..aaaa9737b 100644 --- a/warpgate-admin/src/api/targets.rs +++ b/warpgate-admin/src/api/targets.rs @@ -61,7 +61,7 @@ impl ListApi { ) -> Result { require_admin_permission(&ctx, None).await?; - let db = ctx.services.db.lock().await; + let db = ctx.services().db.lock().await; let mut targets = Target::Entity::find(); @@ -113,7 +113,7 @@ impl ListApi { return Ok(CreateTargetResponse::BadRequest(Json("name".into()))); } - let db = ctx.services.db.lock().await; + let db = ctx.services().db.lock().await; let existing = Target::Entity::find() .filter(Target::Column::Name.eq(body.name.clone())) .one(&*db) @@ -196,7 +196,7 @@ impl DetailApi { ) -> Result { require_admin_permission(&ctx, None).await?; - let db = ctx.services.db.lock().await; + let db = ctx.services().db.lock().await; let Some(target) = Target::Entity::find_by_id(id.0).one(&*db).await? else { return Ok(GetTargetResponse::NotFound); @@ -215,7 +215,7 @@ impl DetailApi { ) -> Result { require_admin_permission(&ctx, Some(AdminPermission::TargetsEdit)).await?; - let db = ctx.services.db.lock().await; + let db = ctx.services().db.lock().await; let Some(target) = Target::Entity::find_by_id(id.0).one(&*db).await? else { return Ok(UpdateTargetResponse::NotFound); @@ -225,7 +225,7 @@ impl DetailApi { return Ok(UpdateTargetResponse::BadRequest); } - let services = &ctx.services; + let services = ctx.services(); let mut model: Target::ActiveModel = target.into(); model.name = Set(body.name.clone()); model.description = Set(body.description.clone().unwrap_or_default()); @@ -262,7 +262,7 @@ impl DetailApi { ) -> Result { require_admin_permission(&ctx, Some(AdminPermission::TargetsDelete)).await?; - let db = ctx.services.db.lock().await; + let db = ctx.services().db.lock().await; let Some(target) = Target::Entity::find_by_id(id.0).one(&*db).await? else { return Ok(DeleteTargetResponse::NotFound); @@ -307,7 +307,7 @@ impl DetailApi { ) -> Result { require_admin_permission(&ctx, Some(AdminPermission::TargetsEdit)).await?; - let db = ctx.services.db.lock().await; + let db = ctx.services().db.lock().await; let Some(target) = Target::Entity::find_by_id(id.0).one(&*db).await? else { return Ok(TargetKnownSshHostKeysResponse::NotFound); @@ -374,7 +374,7 @@ impl RolesApi { ) -> Result { require_admin_permission(&ctx, None).await?; - let db = ctx.services.db.lock().await; + let db = ctx.services().db.lock().await; let Some((_, roles)) = Target::Entity::find_by_id(*id) .find_with_related(Role::Entity) @@ -405,7 +405,7 @@ impl RolesApi { ) -> Result { require_admin_permission(&ctx, Some(AdminPermission::AccessRolesAssign)).await?; - let db = ctx.services.db.lock().await; + let db = ctx.services().db.lock().await; if !TargetRoleAssignment::Entity::find() .filter(TargetRoleAssignment::Column::TargetId.eq(id.0)) @@ -443,7 +443,7 @@ impl RolesApi { ) -> Result { require_admin_permission(&ctx, Some(AdminPermission::AccessRolesAssign)).await?; - let db = ctx.services.db.lock().await; + let db = ctx.services().db.lock().await; let Some(model) = TargetRoleAssignment::Entity::find() .filter(TargetRoleAssignment::Column::TargetId.eq(id.0)) diff --git a/warpgate-admin/src/api/tickets_detail.rs b/warpgate-admin/src/api/tickets_detail.rs index b1dfdb3d7..2acf0b6d1 100644 --- a/warpgate-admin/src/api/tickets_detail.rs +++ b/warpgate-admin/src/api/tickets_detail.rs @@ -39,7 +39,7 @@ impl Api { require_admin_permission(&ctx, Some(AdminPermission::TicketsDelete)).await?; - let db = ctx.services.db.lock().await; + let db = ctx.services().db.lock().await; let Some(ticket) = Ticket::Entity::find_by_id(id.0).one(&*db).await? else { return Ok(DeleteTicketResponse::NotFound); diff --git a/warpgate-admin/src/api/tickets_list.rs b/warpgate-admin/src/api/tickets_list.rs index 7e599cb8d..8f0a48a80 100644 --- a/warpgate-admin/src/api/tickets_list.rs +++ b/warpgate-admin/src/api/tickets_list.rs @@ -108,7 +108,7 @@ impl Api { require_admin_permission(&ctx, None).await?; - let db = ctx.services.db.lock().await; + let db = ctx.services().db.lock().await; let tickets = Ticket::Entity::find().all(&*db).await?; let tickets = futures::future::join_all( tickets @@ -132,7 +132,7 @@ impl Api { require_admin_permission(&ctx, Some(AdminPermission::TicketsCreate)).await?; - let db = ctx.services.db.lock().await; + let db = ctx.services().db.lock().await; let Some(user) = (if let Some(user_id) = body.user_id { User::Entity::find_by_id(user_id).one(&*db).await? diff --git a/warpgate-admin/src/api/users.rs b/warpgate-admin/src/api/users.rs index 48c6deca7..ae6f64828 100644 --- a/warpgate-admin/src/api/users.rs +++ b/warpgate-admin/src/api/users.rs @@ -61,7 +61,7 @@ impl ListApi { ) -> Result { require_admin_permission(&ctx, None).await?; - let db = ctx.services.db.lock().await; + let db = ctx.services().db.lock().await; let mut users = User::Entity::find().order_by_asc(User::Column::Username); @@ -93,7 +93,7 @@ impl ListApi { return Ok(CreateUserResponse::BadRequest(Json("name".into()))); } - let db = ctx.services.db.lock().await; + let db = ctx.services().db.lock().await; let values = User::ActiveModel { id: Set(Uuid::new_v4()), @@ -185,7 +185,7 @@ impl DetailApi { ) -> Result { require_admin_permission(&ctx, None).await?; - let db = ctx.services.db.lock().await; + let db = ctx.services().db.lock().await; let Some(user) = User::Entity::find_by_id(id.0).one(&*db).await? else { return Ok(GetUserResponse::NotFound); @@ -204,7 +204,7 @@ impl DetailApi { ) -> Result { require_admin_permission(&ctx, Some(AdminPermission::UsersEdit)).await?; - let db = ctx.services.db.lock().await; + let db = ctx.services().db.lock().await; let Some(user) = User::Entity::find_by_id(id.0).one(&*db).await? else { return Ok(UpdateUserResponse::NotFound); @@ -221,11 +221,11 @@ impl DetailApi { drop(db); - ctx.services + ctx.services() .rate_limiter_registry .lock() .await - .apply_new_rate_limits(&*ctx.services.state.lock().await) + .apply_new_rate_limits(&*ctx.services().state.lock().await) .await?; Ok(UpdateUserResponse::Ok(Json(user.try_into()?))) @@ -240,7 +240,7 @@ impl DetailApi { ) -> Result { require_admin_permission(&ctx, Some(AdminPermission::UsersDelete)).await?; - let db = ctx.services.db.lock().await; + let db = ctx.services().db.lock().await; let Some(user) = User::Entity::find_by_id(id.0).one(&*db).await? else { return Ok(DeleteUserResponse::NotFound); @@ -281,7 +281,7 @@ impl DetailApi { ) -> Result { require_admin_permission(&ctx, Some(AdminPermission::UsersEdit)).await?; - let db = ctx.services.db.lock().await; + let db = ctx.services().db.lock().await; let Some(user) = User::Entity::find_by_id(id.0).one(&*db).await? else { return Ok(UnlinkUserFromLdapResponse::NotFound); @@ -316,7 +316,7 @@ impl DetailApi { require_admin_permission(&ctx, Some(AdminPermission::UsersEdit)).await?; - let db = ctx.services.db.lock().await; + let db = ctx.services().db.lock().await; let Some(user) = User::Entity::find_by_id(id.0).one(&*db).await? else { return Ok(AutoLinkUserToLdapResponse::NotFound); @@ -510,7 +510,7 @@ impl RolesApi { ) -> Result { require_admin_permission(&ctx, None).await?; - let db = ctx.services.db.lock().await; + let db = ctx.services().db.lock().await; let Some(_user) = User::Entity::find_by_id(*id).one(&*db).await? else { return Ok(GetUserRolesResponse::NotFound); @@ -549,7 +549,7 @@ impl RolesApi { _sec_scheme: AnySecurityScheme, ) -> Result { require_admin_permission(&ctx, None).await?; - let db = ctx.services.db.lock().await; + let db = ctx.services().db.lock().await; let Some(assignment) = UserRoleAssignment::Entity::find() .filter(UserRoleAssignment::Column::UserId.eq(id.0)) @@ -586,7 +586,7 @@ impl RolesApi { ) -> Result { require_admin_permission(&ctx, Some(AdminPermission::AccessRolesAssign)).await?; - let db = ctx.services.db.lock().await; + let db = ctx.services().db.lock().await; let expires_at = body.0.and_then(|b| b.expires_at); let Some(grantee) = User::Entity::find_by_id(id.0).one(&*db).await? else { @@ -598,7 +598,7 @@ impl RolesApi { }; let assignment = - UserRoleAssignment::Entity::idempotent_grant(&*db, id.0, role_id.0, expires_at).await?; + UserRoleAssignment::Entity::idempotent_grant(&db, id.0, role_id.0, expires_at).await?; AuditEvent::AccessRoleGranted { grantee_id: grantee.id, @@ -630,7 +630,7 @@ impl RolesApi { ) -> Result { require_admin_permission(&ctx, Some(AdminPermission::AccessRolesAssign)).await?; - let db = ctx.services.db.lock().await; + let db = ctx.services().db.lock().await; let Some(grantee) = User::Entity::find_by_id(id.0).one(&*db).await? else { return Ok(DeleteUserRoleResponse::NotFound); @@ -682,7 +682,7 @@ impl RolesApi { _sec_scheme: AnySecurityScheme, ) -> Result { require_admin_permission(&ctx, None).await?; - let db = ctx.services.db.lock().await; + let db = ctx.services().db.lock().await; let Some(role) = Role::Entity::find_by_id(role_id.0).one(&*db).await? else { return Ok(UpdateUserRoleResponse::NotFound); @@ -721,7 +721,7 @@ impl RolesApi { ) -> Result { require_admin_permission(&ctx, None).await?; - let db = ctx.services.db.lock().await; + let db = ctx.services().db.lock().await; let Some((_, roles)) = User::Entity::find_by_id(*id) .find_with_related(AdminRole::Entity) @@ -752,7 +752,7 @@ impl RolesApi { ) -> Result { require_admin_permission(&ctx, Some(AdminPermission::AdminRolesManage)).await?; - let db = ctx.services.db.lock().await; + let db = ctx.services().db.lock().await; let Some(grantee) = User::Entity::find_by_id(id.0).one(&*db).await? else { return Ok(AddUserAdminRoleResponse::NotFound); @@ -810,7 +810,7 @@ impl RolesApi { ) -> Result { require_admin_permission(&ctx, Some(AdminPermission::AdminRolesManage)).await?; - let db = ctx.services.db.lock().await; + let db = ctx.services().db.lock().await; let Some(grantee) = User::Entity::find_by_id(id.0).one(&*db).await? else { return Ok(DeleteUserAdminRoleResponse::NotFound); diff --git a/warpgate-common-http/src/auth.rs b/warpgate-common-http/src/auth.rs index 74d93f038..870decfe6 100644 --- a/warpgate-common-http/src/auth.rs +++ b/warpgate-common-http/src/auth.rs @@ -1,5 +1,11 @@ +use std::ops::Deref; + +use poem::http::header::HOST; +use poem::http::uri::Scheme; +use poem::Request; use serde::{Deserialize, Serialize}; use uuid::Uuid; +use warpgate_common::http_headers::{X_FORWARDED_HOST, X_FORWARDED_PROTO}; #[derive(Clone, Serialize, Deserialize)] pub struct AuthStateId(pub Uuid); @@ -42,16 +48,83 @@ pub enum RequestAuthorization { #[derive(Clone)] pub struct UnauthenticatedRequestContext { - pub services: warpgate_core::Services, + services: warpgate_core::Services, + should_trust_x_forwarded: bool, } /// Provided to API handlers as Data<> impl UnauthenticatedRequestContext { + pub async fn new(services: warpgate_core::Services) -> Self { + let should_trust_x_forwarded = services + .config + .lock() + .await + .store + .http + .trust_x_forwarded_headers; + Self { + services, + should_trust_x_forwarded, + } + } + + pub const fn services(&self) -> &warpgate_core::Services { + &self.services + } + pub fn to_authenticated(&self, auth: RequestAuthorization) -> AuthenticatedRequestContext { AuthenticatedRequestContext { auth, - services: self.services.clone(), + inner: self.clone(), + } + } + + /// Returns the trusted full Host header value (including port if present), + /// preferring X-Forwarded-Host if trust_x_forwarded_headers is enabled in config. + pub fn trusted_host_header(&self, req: &Request) -> Option { + let mut host = req.header(HOST).map(ToString::to_string).or_else(|| { + let uri = req.original_uri(); + let h = uri.host()?; + Some(match uri.port() { + Some(port) => format!("{h}:{port}"), + None => h.to_string(), + }) + }); + + if self.should_trust_x_forwarded { + if let Some(xfh) = req.header(&X_FORWARDED_HOST) { + host = Some(xfh.to_string()); + } + } + + host + } + + /// Returns the trusted hostname only (port stripped), + /// preferring X-Forwarded-Host if trust_x_forwarded_headers is enabled in config. + pub fn trusted_hostname(&self, req: &Request) -> Option { + self.trusted_host_header(req) + .map(|h| h.split(':').next().unwrap_or(&h).to_string()) + } + + /// Returns the trusted protocol scheme for the request, preferring X-Forwarded-Proto + /// if trust_x_forwarded_headers is enabled in config. + pub fn trusted_proto(&self, req: &Request) -> Scheme { + let mut scheme = req + .original_uri() + .scheme() + .cloned() + .unwrap_or(Scheme::HTTPS); + + if self.should_trust_x_forwarded { + if let Some(proto) = req.header(&X_FORWARDED_PROTO) { + if let Ok(s) = Scheme::try_from(proto) { + scheme = s; + } + } } + + scheme } } @@ -59,7 +132,15 @@ impl UnauthenticatedRequestContext { /// Provided to API handlers as Data<> when a request is authenticated pub struct AuthenticatedRequestContext { pub auth: RequestAuthorization, - pub services: warpgate_core::Services, + inner: UnauthenticatedRequestContext, +} + +impl Deref for AuthenticatedRequestContext { + type Target = UnauthenticatedRequestContext; + + fn deref(&self) -> &Self::Target { + &self.inner + } } impl RequestAuthorization { diff --git a/warpgate-core/src/config_providers/db.rs b/warpgate-core/src/config_providers/db.rs index 79ed934de..2aa3365f4 100644 --- a/warpgate-core/src/config_providers/db.rs +++ b/warpgate-core/src/config_providers/db.rs @@ -601,7 +601,7 @@ impl ConfigProvider for DatabaseConfigProvider { (None, true) => { info!("Adding role {role_name} for user {username} (from SSO)"); entities::UserRoleAssignment::Entity::idempotent_grant( - &*db, user.id, role.id, None, + &db, user.id, role.id, None, ) .await?; } diff --git a/warpgate-db-entities/src/UserRoleAssignment.rs b/warpgate-db-entities/src/UserRoleAssignment.rs index 0b2178205..5533029d9 100644 --- a/warpgate-db-entities/src/UserRoleAssignment.rs +++ b/warpgate-db-entities/src/UserRoleAssignment.rs @@ -47,10 +47,10 @@ impl Entity { role_id: Uuid, expires_at: Option, ) -> Result { - let existing = Entity::find() + let existing = Self::find() .filter(Column::UserId.eq(user_id)) .filter(Column::RoleId.eq(role_id)) - .one(&*db) + .one(db) .await?; let now = OffsetDateTime::now_utc(); @@ -64,7 +64,7 @@ impl Entity { model.granted_at = Set(Some(now)); model.expires_at = Set(expires_at); model.revoked_at = Set(None); - model.update(&*db).await? + model.update(db).await? } else { let values = ActiveModel { user_id: Set(user_id), @@ -74,7 +74,7 @@ impl Entity { revoked_at: Set(None), ..Default::default() }; - values.insert(&*db).await? + values.insert(db).await? }) } } diff --git a/warpgate-protocol-http/src/api/api_tokens.rs b/warpgate-protocol-http/src/api/api_tokens.rs index 5ed285f72..135b5912b 100644 --- a/warpgate-protocol-http/src/api/api_tokens.rs +++ b/warpgate-protocol-http/src/api/api_tokens.rs @@ -85,7 +85,7 @@ impl Api { ctx: Data<&AuthenticatedRequestContext>, ) -> Result { let auth = &ctx.auth; - let db = ctx.services.db.lock().await; + let db = ctx.services().db.lock().await; let Some(user_model) = get_user(auth, &db).await? else { return Ok(GetApiTokensResponse::Unauthorized); @@ -110,7 +110,7 @@ impl Api { body: Json, ) -> Result { let auth = &ctx.auth; - let db = ctx.services.db.lock().await; + let db = ctx.services().db.lock().await; let Some(user_model) = get_user(auth, &db).await? else { return Ok(CreateApiTokenResponse::Unauthorized); @@ -147,7 +147,7 @@ impl Api { id: Path, ) -> Result { let auth = &ctx.auth; - let db = ctx.services.db.lock().await; + let db = ctx.services().db.lock().await; let Some(user_model) = get_user(auth, &db).await? else { return Ok(DeleteApiTokenResponse::Unauthorized); diff --git a/warpgate-protocol-http/src/api/auth.rs b/warpgate-protocol-http/src/api/auth.rs index 1a016ef3d..819ad6b23 100644 --- a/warpgate-protocol-http/src/api/auth.rs +++ b/warpgate-protocol-http/src/api/auth.rs @@ -140,7 +140,7 @@ impl Api { ctx: Data<&UnauthenticatedRequestContext>, body: Json, ) -> poem::Result { - let services = &ctx.services; + let services = ctx.services(); let mut auth_state_store = services.auth_state_store.lock().await; let state_arc = match get_auth_state_for_request( &body.username, @@ -191,7 +191,7 @@ impl Api { ctx: Data<&UnauthenticatedRequestContext>, body: Json, ) -> poem::Result { - let services = &ctx.services; + let services = ctx.services(); let state_id = session.get_auth_state_id(); let mut auth_state_store = services.auth_state_store.lock().await; @@ -248,7 +248,7 @@ impl Api { session: &Session, ctx: Data<&UnauthenticatedRequestContext>, ) -> poem::Result { - let services = &ctx.services; + let services = ctx.services(); let Some(state_id) = session.get_auth_state_id() else { return Ok(AuthStateResponse::NotFound); }; @@ -272,7 +272,7 @@ impl Api { session: &Session, ctx: Data<&UnauthenticatedRequestContext>, ) -> poem::Result { - let services = &ctx.services; + let services = ctx.services(); let Some(state_id) = session.get_auth_state_id() else { return Ok(AuthStateResponse::NotFound); }; @@ -301,7 +301,7 @@ impl Api { ctx: Data<&AuthenticatedRequestContext>, _sec_scheme: AnySecurityScheme, ) -> poem::Result { - let services = &ctx.services; + let services = ctx.services(); let store = services.auth_state_store.lock().await; let RequestAuthorization::Session(SessionAuthorization::User { username, .. }) = &ctx.auth @@ -331,7 +331,7 @@ impl Api { ctx: Data<&AuthenticatedRequestContext>, id: Path, ) -> poem::Result { - let services = &ctx.services; + let services = ctx.services(); let state_arc = get_auth_state(&id, &ctx).await; let Some(state_arc) = state_arc else { return Ok(AuthStateResponse::NotFound); @@ -354,7 +354,7 @@ impl Api { id: Path, _sec_scheme: AnySecurityScheme, ) -> poem::Result { - let services = &ctx.services; + let services = ctx.services(); let Some(state_arc) = get_auth_state(&id, &ctx).await else { return Ok(AuthStateResponse::NotFound); }; @@ -387,7 +387,7 @@ impl Api { id: Path, _sec_scheme: AnySecurityScheme, ) -> poem::Result { - let services = &ctx.services; + let services = ctx.services(); let Some(state_arc) = get_auth_state(&id, &ctx).await else { return Ok(AuthStateResponse::NotFound); }; @@ -404,7 +404,7 @@ async fn get_auth_state( id: &Uuid, ctx: &AuthenticatedRequestContext, ) -> Option>> { - let store = ctx.services.auth_state_store.lock().await; + let store = ctx.services().auth_state_store.lock().await; let RequestAuthorization::Session(SessionAuthorization::User { username, .. }) = &ctx.auth else { @@ -454,7 +454,7 @@ pub async fn api_get_web_auth_requests_stream( ws: WebSocket, ctx: Data<&AuthenticatedRequestContext>, ) -> anyhow::Result { - let services = &ctx.services; + let services = ctx.services(); let auth_state_store = services.auth_state_store.clone(); let username = match &ctx.auth { diff --git a/warpgate-protocol-http/src/api/credentials.rs b/warpgate-protocol-http/src/api/credentials.rs index a0a63b64a..ba6195455 100644 --- a/warpgate-protocol-http/src/api/credentials.rs +++ b/warpgate-protocol-http/src/api/credentials.rs @@ -219,7 +219,7 @@ enum DeleteCertificateCredentialResponse { pub fn parameters_based_auth(e: E) -> impl Endpoint { e.around(|ep, req| async move { let ctx = Data::<&UnauthenticatedRequestContext>::from_request_without_body(&req).await?; - let services = &ctx.services; + let services = ctx.services(); let parameters = Parameters::Entity::get(&*services.db.lock().await) .await .map_err(WarpgateError::from)?; @@ -247,7 +247,7 @@ impl Api { _sec_scheme: AnySecurityScheme, ) -> Result { let auth = &ctx.auth; - let db = ctx.services.db.lock().await; + let db = ctx.services().db.lock().await; let Some(user) = get_user(auth, &db).await? else { return Ok(CredentialsStateResponse::Unauthorized); @@ -306,7 +306,7 @@ impl Api { _sec_scheme: AnySecurityScheme, ) -> Result { let auth = &ctx.auth; - let db = ctx.services.db.lock().await; + let db = ctx.services().db.lock().await; let Some(user) = get_user(auth, &db).await? else { return Ok(ChangePasswordResponse::Unauthorized); @@ -364,7 +364,7 @@ impl Api { _sec_scheme: AnySecurityScheme, ) -> Result { let auth = &ctx.auth; - let db = ctx.services.db.lock().await; + let db = ctx.services().db.lock().await; let Some(user) = get_user(auth, &db).await? else { return Ok(CreatePublicKeyCredentialResponse::Unauthorized); @@ -411,7 +411,7 @@ impl Api { _sec_scheme: AnySecurityScheme, ) -> Result { let auth = &ctx.auth; - let db = ctx.services.db.lock().await; + let db = ctx.services().db.lock().await; let Some(user) = get_user(auth, &db).await? else { return Ok(DeleteCredentialResponse::Unauthorized); @@ -454,7 +454,7 @@ impl Api { _sec_scheme: AnySecurityScheme, ) -> Result { let auth = &ctx.auth; - let db = ctx.services.db.lock().await; + let db = ctx.services().db.lock().await; let Some(user) = get_user(auth, &db).await? else { return Ok(CreateOtpCredentialResponse::Unauthorized); @@ -510,7 +510,7 @@ impl Api { _sec_scheme: AnySecurityScheme, ) -> Result { let auth = &ctx.auth; - let db = ctx.services.db.lock().await; + let db = ctx.services().db.lock().await; let Some(user) = get_user(auth, &db).await? else { return Ok(DeleteCredentialResponse::Unauthorized); @@ -552,7 +552,7 @@ impl Api { body: Json, ) -> Result { let auth = &ctx.auth; - let db = ctx.services.db.lock().await; + let db = ctx.services().db.lock().await; let Some(user) = get_user(auth, &db).await? else { return Ok(IssueCertificateCredentialResponse::Unauthorized); @@ -610,7 +610,7 @@ impl Api { id: Path, ) -> Result { let auth = &ctx.auth; - let db = ctx.services.db.lock().await; + let db = ctx.services().db.lock().await; let Some(user) = get_user(auth, &db).await? else { return Ok(DeleteCertificateCredentialResponse::Unauthorized); diff --git a/warpgate-protocol-http/src/api/info.rs b/warpgate-protocol-http/src/api/info.rs index 3c7842b2c..09abc479f 100644 --- a/warpgate-protocol-http/src/api/info.rs +++ b/warpgate-protocol-http/src/api/info.rs @@ -106,7 +106,7 @@ impl Api { ctx: Data<&UnauthenticatedRequestContext>, auth_ctx: Option>, ) -> poem::Result { - let config = ctx.services.config.lock().await; + let config = ctx.services().config.lock().await; let external_host = config .construct_external_url(Some(req), None) .ok() @@ -115,14 +115,14 @@ impl Api { .map(|x| x.to_string()); let parameters = { - Parameters::Entity::get(&*ctx.services.db.lock().await) + Parameters::Entity::get(&*ctx.services().db.lock().await) .await .context("loading parameters")? }; let setup_state = { let (users, targets) = { - let mut p = ctx.services.config_provider.lock().await; + let mut p = ctx.services().config_provider.lock().await; let users = p.list_users().await?; let targets = p.list_targets().await?; (users, targets) @@ -148,7 +148,7 @@ impl Api { }; let has_ldap = LdapServer::Entity::find() - .one(&*ctx.services.db.lock().await) + .one(&*ctx.services().db.lock().await) .await .context("loading LDAP servers")? .is_some(); @@ -195,7 +195,7 @@ impl Api { // compute admin permissions (only if authenticated) let admin_permissions = if let Some(ctx) = &auth_ctx { if let Some(username) = ctx.auth.username() { - let db = ctx.services.db.lock().await; + let db = ctx.services().db.lock().await; let perms = { let mut combined = AdminPermissions::default(); if let Some(user) = User::Entity::find() diff --git a/warpgate-protocol-http/src/api/sso_provider_detail.rs b/warpgate-protocol-http/src/api/sso_provider_detail.rs index d2cfd1142..64cf71c55 100644 --- a/warpgate-protocol-http/src/api/sso_provider_detail.rs +++ b/warpgate-protocol-http/src/api/sso_provider_detail.rs @@ -52,7 +52,7 @@ impl Api { name: Path, next: Query>, ) -> Result { - let config = ctx.services.config.lock().await; + let config = ctx.services().config.lock().await; let name = name.0; @@ -74,7 +74,7 @@ impl Api { let client = SsoClient::new(provider_config.provider.clone())?; let sso_req = client.start_login(return_url.to_string()).await?; - let return_host = req.header("host").map(ToString::to_string); + let return_host = ctx.trusted_host_header(req); let url = sso_req.auth_url().to_string(); session.set( diff --git a/warpgate-protocol-http/src/api/sso_provider_list.rs b/warpgate-protocol-http/src/api/sso_provider_list.rs index 2935f31ce..91267d927 100644 --- a/warpgate-protocol-http/src/api/sso_provider_list.rs +++ b/warpgate-protocol-http/src/api/sso_provider_list.rs @@ -95,7 +95,14 @@ impl Api { &self, ctx: Data<&UnauthenticatedRequestContext>, ) -> Result { - let mut providers = ctx.services.config.lock().await.store.sso_providers.clone(); + let mut providers = ctx + .services() + .config + .lock() + .await + .store + .sso_providers + .clone(); providers.sort_by(|a, b| a.label().cmp(b.label())); Ok(GetSsoProvidersResponse::Ok(Json( providers @@ -171,7 +178,7 @@ impl Api { code: Option<&String>, ) -> Result, WarpgateError> { // pull services locally for convenience - let services = &ctx.services; + let services = ctx.services(); let Some(context) = session.get::(SSO_CONTEXT_SESSION_KEY) else { return Ok(Err("Not in an active SSO process".to_string())); }; @@ -200,7 +207,14 @@ impl Api { info!("SSO login as {email}"); - let providers_config = ctx.services.config.lock().await.store.sso_providers.clone(); + let providers_config = ctx + .services() + .config + .lock() + .await + .store + .sso_providers + .clone(); let mut iter = providers_config.iter(); let Some(provider_config) = iter.find(|x| x.name == context.provider) else { return Ok(Err(format!("No provider matching {}", context.provider))); @@ -356,7 +370,7 @@ impl Api { return Ok(StartSloResponse::NotInSsoSession); }; - let config = ctx.services.config.lock().await; + let config = ctx.services().config.lock().await; let return_url = config.construct_external_url(Some(req), None)?; debug!("Return URL: {}", &return_url); diff --git a/warpgate-protocol-http/src/api/targets_list.rs b/warpgate-protocol-http/src/api/targets_list.rs index dad3e196e..f75d3eec9 100644 --- a/warpgate-protocol-http/src/api/targets_list.rs +++ b/warpgate-protocol-http/src/api/targets_list.rs @@ -58,7 +58,7 @@ impl Api { _sec_scheme: AnySecurityScheme, ) -> Result { // Fetch target groups for group information - let services = &ctx.services; + let services = ctx.services(); let groups: Vec = { let db = services.db.lock().await; TargetGroup::Entity::find().all(&*db).await diff --git a/warpgate-protocol-http/src/catchall.rs b/warpgate-protocol-http/src/catchall.rs index 20cb07e58..02640b211 100644 --- a/warpgate-protocol-http/src/catchall.rs +++ b/warpgate-protocol-http/src/catchall.rs @@ -1,6 +1,5 @@ use std::sync::Arc; -use http::header::HOST; use poem::session::Session; use poem::web::websocket::WebSocket; use poem::web::{Data, FromRequest, Redirect}; @@ -50,7 +49,7 @@ pub async fn catchall_endpoint( let span = info_span!("", target=%target.name); Ok(match ws { - Some(ws) => proxy_websocket_request(req, ws, &options) + Some(ws) => proxy_websocket_request(req, ws, &ctx, &options) .instrument(span) .await? .into_response(), @@ -71,14 +70,11 @@ async fn get_target_for_request( let selected_target_name; let need_role_auth; - let request_host = req - .header(HOST) - .map(|h| h.split(':').next().unwrap_or(h).to_string()) - .or_else(|| req.original_uri().host().map(ToString::to_string)); + let request_host = ctx.trusted_hostname(req); let host_based_target_name = if let Some(host) = request_host { let found = ctx - .services + .services() .config_provider .lock() .await @@ -135,7 +131,7 @@ async fn get_target_for_request( if let Some(target_name) = final_target_name { let target = { - ctx.services + ctx.services() .config_provider .lock() .await @@ -153,7 +149,7 @@ async fn get_target_for_request( if let Some(target) = target { if need_role_auth && !ctx - .services + .services() .config_provider .lock() .await diff --git a/warpgate-protocol-http/src/common.rs b/warpgate-protocol-http/src/common.rs index 943a60c63..cd910690c 100644 --- a/warpgate-protocol-http/src/common.rs +++ b/warpgate-protocol-http/src/common.rs @@ -2,7 +2,6 @@ use core::str; use std::sync::Arc; use anyhow::Context; -use http::header::HOST; use http::{HeaderName, StatusCode}; use percent_encoding::{utf8_percent_encode, NON_ALPHANUMERIC}; use poem::error::InternalServerError; @@ -104,7 +103,7 @@ pub struct AuthStateId(pub Uuid); pub async fn is_user_admin(ctx: &AuthenticatedRequestContext) -> poem::Result { // A user is considered an administrator if they have any admin role assigned. - let services = &ctx.services; + let services = ctx.services(); // Admin tokens bypass the database check and are always full administrators. if matches!(ctx.auth, RequestAuthorization::AdminToken) { @@ -259,13 +258,10 @@ pub async fn inject_request_authorization( let mut session_auth = session.get_auth(); if session_auth.is_some() { - let config = ctx.services.config.lock().await; + let config = ctx.services().config.lock().await; if let Ok(base_url) = config.construct_external_url(None, None) { if let Some(base_host) = base_url.host_str() { - let request_host = req - .header(HOST) - .map(|h| h.split(':').next().unwrap_or(h).to_string()) - .or_else(|| req.original_uri().host().map(ToString::to_string)); + let request_host = ctx.trusted_hostname(&req); if let Some(host) = request_host { // Validate request host matches base host or is a subdomain/localhost @@ -295,10 +291,10 @@ pub async fn inject_request_authorization( let token_from_header = token_from_header .to_str() .map_err(poem::error::BadRequest)?; - if Some(token_from_header) == ctx.services.admin_token.lock().await.as_deref() { + if Some(token_from_header) == ctx.services().admin_token.lock().await.as_deref() { Some(RequestAuthorization::AdminToken) } else if let Some(user) = ctx - .services + .services() .config_provider .lock() .await diff --git a/warpgate-protocol-http/src/lib.rs b/warpgate-protocol-http/src/lib.rs index 2d1f403ee..c07f8e7e8 100644 --- a/warpgate-protocol-http/src/lib.rs +++ b/warpgate-protocol-http/src/lib.rs @@ -253,9 +253,9 @@ impl ProtocolServer for HTTPProtocolServer { let span = match handle { Some(ref handle) => { let handle = handle.lock().await; - span_for_request(&req, &ctx.services, Some(&*handle)).await? + span_for_request(&req, ctx.services(), Some(&*handle)).await? } - None => span_for_request(&req, &ctx.services, None).await?, + None => span_for_request(&req, ctx.services(), None).await?, }; ep.call(req).instrument(span).await @@ -273,9 +273,7 @@ impl ProtocolServer for HTTPProtocolServer { session_storage.clone(), )) .with(CookieHostMiddleware::new(base_cookie_domain)) - .data(UnauthenticatedRequestContext { - services: self.services.clone(), - }) + .data(UnauthenticatedRequestContext::new(self.services.clone()).await) .data(session_store.clone()) .data(session_storage); diff --git a/warpgate-protocol-http/src/middleware/cookie_host.rs b/warpgate-protocol-http/src/middleware/cookie_host.rs index da2d080dc..bc442f8be 100644 --- a/warpgate-protocol-http/src/middleware/cookie_host.rs +++ b/warpgate-protocol-http/src/middleware/cookie_host.rs @@ -1,6 +1,8 @@ use cookie::Cookie; use http::uri::Scheme; -use poem::{Endpoint, IntoResponse, Middleware, Request, Response}; +use poem::web::Data; +use poem::{Endpoint, FromRequest, IntoResponse, Middleware, Request, Response}; +use warpgate_common_http::auth::UnauthenticatedRequestContext; use crate::common::{is_localhost_host, SESSION_COOKIE_NAME}; @@ -38,20 +40,9 @@ impl Endpoint for CookieHostMiddlewareEndpoint { type Output = Response; async fn call(&self, req: Request) -> poem::Result { - let host = req - .header(http::header::HOST) - .map(|h| h.split(':').next().unwrap_or(h).to_string()) - .or_else(|| { - req.original_uri() - .host() - .map(std::string::ToString::to_string) - }); - - let scheme_https = req.original_uri().scheme() == Some(&Scheme::HTTPS); - let header_https = req - .header("x-forwarded-proto") - .is_some_and(|h| h == "https"); - let is_https = scheme_https || header_https; + let ctx = Data::<&UnauthenticatedRequestContext>::from_request_without_body(&req).await?; + let host = ctx.trusted_hostname(&req); + let is_https = ctx.trusted_proto(&req) == Scheme::HTTPS; let mut resp = self.inner.call(req).await?.into_response(); diff --git a/warpgate-protocol-http/src/middleware/ticket.rs b/warpgate-protocol-http/src/middleware/ticket.rs index 1c179a7cd..3a6aaea8e 100644 --- a/warpgate-protocol-http/src/middleware/ticket.rs +++ b/warpgate-protocol-http/src/middleware/ticket.rs @@ -64,9 +64,9 @@ impl Endpoint for TicketMiddlewareEndpoint { if let Some((_ticket_model, target, user_info)) = { let ticket_secret = Secret::new(ticket); if let Some((ticket, target, user_info)) = - authorize_ticket(&ctx.services.db, &ticket_secret).await? + authorize_ticket(&ctx.services().db, &ticket_secret).await? { - consume_ticket(&ctx.services.db, &ticket.id).await?; + consume_ticket(&ctx.services().db, &ticket.id).await?; Some((ticket, target, user_info)) } else { None diff --git a/warpgate-protocol-http/src/proxy.rs b/warpgate-protocol-http/src/proxy.rs index fa6739318..a55e4859f 100644 --- a/warpgate-protocol-http/src/proxy.rs +++ b/warpgate-protocol-http/src/proxy.rs @@ -211,19 +211,19 @@ fn copy_server_request(req: &Request, mut target: B) -> B target } -fn inject_forwarding_headers(req: &Request, mut target: B) -> Result { - #[allow(clippy::unwrap_used)] - if let Some(host) = req.headers().get(http::header::HOST) { - target = target.header( - X_FORWARDED_HOST.clone(), - host.to_str()?.split(':').next().unwrap(), - ); +fn inject_forwarding_headers( + req: &Request, + ctx: &AuthenticatedRequestContext, + mut target: B, +) -> B { + if let Some(host) = ctx.trusted_host_header(req) { + target = target.header(X_FORWARDED_HOST.clone(), host); } - target = target.header(X_FORWARDED_PROTO.clone(), req.scheme().as_str()); + target = target.header(X_FORWARDED_PROTO.clone(), ctx.trusted_proto(req).as_str()); if let Some(addr) = req.remote_addr().as_socket_addr() { target = target.header(X_FORWARDED_FOR.clone(), addr.ip().to_string()); } - Ok(target) + target } async fn inject_own_headers(req: &Request, mut target: B) -> Result { @@ -286,7 +286,7 @@ pub async fn proxy_normal_request( let mut client_request = client.request(req.method().into(), uri.to_string()); client_request = copy_server_request(req, client_request); - client_request = inject_forwarding_headers(req, client_request)?; + client_request = inject_forwarding_headers(req, ctx, client_request); client_request = inject_own_headers(req, client_request).await?; client_request = rewrite_request(client_request, options)?; if let Some(authorization_header) = authorization_header { @@ -310,7 +310,7 @@ pub async fn proxy_normal_request( log_request_result( req.method(), req.original_uri(), - get_client_ip(req, &ctx.services).await.as_deref(), + get_client_ip(req, ctx.services()).await.as_deref(), status, ); @@ -380,10 +380,11 @@ async fn copy_client_body_and_embed( pub async fn proxy_websocket_request( req: &Request, ws: WebSocket, + ctx: &AuthenticatedRequestContext, options: &TargetHTTPOptions, ) -> poem::Result { let uri = construct_uri(req, options, true)?; - proxy_ws_inner(req, ws, uri.clone(), options) + proxy_ws_inner(req, ws, uri.clone(), ctx, options) .await .map_err(|error| { tracing::error!(?uri, ?error, "WebSocket proxy failed"); @@ -425,6 +426,7 @@ async fn proxy_ws_inner( req: &Request, ws: WebSocket, uri: Uri, + ctx: &AuthenticatedRequestContext, options: &TargetHTTPOptions, ) -> poem::Result { let (authorization_header, uri) = extract_basic_auth(uri)?; @@ -451,7 +453,7 @@ async fn proxy_ws_inner( } client_request = copy_server_request(req, client_request); - client_request = inject_forwarding_headers(req, client_request)?; + client_request = inject_forwarding_headers(req, ctx, client_request); client_request = inject_own_headers(req, client_request).await?; client_request = rewrite_request(client_request, options)?; diff --git a/warpgate-protocol-http/src/session.rs b/warpgate-protocol-http/src/session.rs index 3e087a25a..4ccea7d6f 100644 --- a/warpgate-protocol-http/src/session.rs +++ b/warpgate-protocol-http/src/session.rs @@ -109,7 +109,7 @@ impl SessionStore { let (session_handle, mut session_handle_rx) = HttpSessionHandle::new(); let server_handle = State::register_session( - &ctx.services.state, + &ctx.services().state, &PROTOCOL_NAME, SessionStateInit { remote_address: remote_address.0.as_socket_addr().copied(), diff --git a/warpgate-protocol-kubernetes/src/server/handlers.rs b/warpgate-protocol-kubernetes/src/server/handlers.rs index a2d6c79e6..ac5da5798 100644 --- a/warpgate-protocol-kubernetes/src/server/handlers.rs +++ b/warpgate-protocol-kubernetes/src/server/handlers.rs @@ -60,7 +60,8 @@ pub async fn handle_api_request( "Handling Kubernetes API request" ); - let (user_info, target) = authenticate_and_get_target(req, &target_name, &ctx.services).await?; + let (user_info, target) = + authenticate_and_get_target(req, &target_name, ctx.services()).await?; let TargetOptions::Kubernetes(k8s_options) = &target.options else { return Err(poem::Error::from_string( @@ -82,7 +83,7 @@ pub async fn handle_api_request( handle.set_target(&target).await?; ( handle.id(), - span_for_request(req, &ctx.services, Some(&*handle)).await?, + span_for_request(req, ctx.services(), Some(&*handle)).await?, ) }; @@ -95,7 +96,7 @@ pub async fn handle_api_request( &path, user_info, session_id, - &ctx.services, + ctx.services(), ) .await .map(IntoResponse::into_response) @@ -107,14 +108,14 @@ pub async fn handle_api_request( &path, user_info, session_id, - &ctx.services, + ctx.services(), ) .await .map(IntoResponse::into_response) .context("handling Kubernetes API request") }; - let client_ip = get_client_ip(req, &ctx.services).await; + let client_ip = get_client_ip(req, ctx.services()).await; let response = response.inspect_err(|e| { log_request_error(req.method(), req.original_uri(), client_ip.as_deref(), e); })?; diff --git a/warpgate-protocol-kubernetes/src/server/mod.rs b/warpgate-protocol-kubernetes/src/server/mod.rs index 5dcdcf7de..ed9e80d36 100644 --- a/warpgate-protocol-kubernetes/src/server/mod.rs +++ b/warpgate-protocol-kubernetes/src/server/mod.rs @@ -29,9 +29,7 @@ pub async fn run_server(services: Services, address: ListenEndpoint) -> Result<( .at("/:target_name/*path", handle_api_request) .with(poem::middleware::Cors::new()) .with(CertificateExtractorMiddleware) - .data(UnauthenticatedRequestContext { - services: services.clone(), - }) + .data(UnauthenticatedRequestContext::new(services.clone()).await) .data(correlator); info!(?address, "Kubernetes protocol listening"); diff --git a/warpgate/src/commands/create_user.rs b/warpgate/src/commands/create_user.rs index 3e5a3d939..bd48c8136 100644 --- a/warpgate/src/commands/create_user.rs +++ b/warpgate/src/commands/create_user.rs @@ -56,8 +56,7 @@ pub async fn command( .one(&*db) .await? { - UserRoleAssignment::Entity::idempotent_grant(&*db, db_user.id, db_role.id, None) - .await?; + UserRoleAssignment::Entity::idempotent_grant(&db, db_user.id, db_role.id, None).await?; } // admin role From a7b0161da37c6d4638a92037b4d657f53c9b82ef Mon Sep 17 00:00:00 2001 From: Lars <60571459+LarsSven@users.noreply.github.com> Date: Sun, 12 Apr 2026 23:53:04 +0200 Subject: [PATCH 003/556] Use constant time comparison for admin tokens (#1853) Co-authored-by: Eugene --- Cargo.lock | 1 + warpgate-protocol-http/Cargo.toml | 1 + warpgate-protocol-http/src/common.rs | 13 ++++++++++++- 3 files changed, 14 insertions(+), 1 deletion(-) diff --git a/Cargo.lock b/Cargo.lock index 0655fc54a..7f4e34590 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -6844,6 +6844,7 @@ dependencies = [ "sea-orm", "serde", "serde_json", + "subtle", "time", "tokio", "tokio-tungstenite", diff --git a/warpgate-protocol-http/Cargo.toml b/warpgate-protocol-http/Cargo.toml index 6af1f91e0..46100410f 100644 --- a/warpgate-protocol-http/Cargo.toml +++ b/warpgate-protocol-http/Cargo.toml @@ -34,6 +34,7 @@ warpgate-db-entities = { path = "../warpgate-db-entities", default-features = fa warpgate-web = { path = "../warpgate-web", default-features = false } warpgate-sso = { path = "../warpgate-sso", default-features = false } percent-encoding = { version = "2.1", default-features = false } +subtle = "2" uuid.workspace = true regex.workspace = true url = { version = "2.4", default-features = false } diff --git a/warpgate-protocol-http/src/common.rs b/warpgate-protocol-http/src/common.rs index cd910690c..49e208a78 100644 --- a/warpgate-protocol-http/src/common.rs +++ b/warpgate-protocol-http/src/common.rs @@ -10,6 +10,7 @@ use poem::web::{Data, Redirect}; use poem::{Endpoint, EndpointExt, FromRequest, IntoResponse, Request, Response}; use sea_orm::{ColumnTrait, EntityTrait, PaginatorTrait, QueryFilter}; use serde::{Deserialize, Serialize}; +use subtle::ConstantTimeEq; use tokio::sync::Mutex; use uuid::Uuid; use warpgate_common::auth::{AuthState, AuthStateUserInfo, CredentialKind}; @@ -291,7 +292,17 @@ pub async fn inject_request_authorization( let token_from_header = token_from_header .to_str() .map_err(poem::error::BadRequest)?; - if Some(token_from_header) == ctx.services().admin_token.lock().await.as_deref() { + if ctx + .services() + .admin_token + .lock() + .await + .as_deref() + .is_some_and(|admin_token| { + // Use constant time comparison to prevent timing attacks + admin_token.as_bytes().ct_eq(token_from_header.as_bytes()).into() + }) + { Some(RequestAuthorization::AdminToken) } else if let Some(user) = ctx .services() From 332a90a169e0633c674dd5f3afa2bc662d4e6b51 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Sun, 12 Apr 2026 23:53:50 +0200 Subject: [PATCH 004/556] Bump basic-ftp from 5.2.0 to 5.2.2 in /warpgate-web (#1856) Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> --- warpgate-web/package-lock.json | 24 ++++-------------------- 1 file changed, 4 insertions(+), 20 deletions(-) diff --git a/warpgate-web/package-lock.json b/warpgate-web/package-lock.json index e4e6146ca..63675916d 100644 --- a/warpgate-web/package-lock.json +++ b/warpgate-web/package-lock.json @@ -1083,7 +1083,6 @@ "integrity": "sha512-JSIeW+USuMJkkcNbiOdcPkVCeI3TSnXstIVEPpp3HiaKnPRuSbUUKm9TY9o/XpIcPHWUOQItAtC5BiAwFdVITQ==", "dev": true, "license": "MIT", - "peer": true, "dependencies": { "file-type": "21.3.0", "iterare": "1.2.1", @@ -1298,7 +1297,6 @@ "integrity": "sha512-P1st0aksCrn9sGZhp8GMYwBnQsbvAWsZAX44oXNNvLHGqAOcoVxmjZiohstwQ7SqKnbR47akdNi+uleWD8+g6A==", "dev": true, "license": "MIT", - "peer": true, "funding": { "type": "opencollective", "url": "https://opencollective.com/popperjs" @@ -1697,7 +1695,6 @@ "integrity": "sha512-ou/d51QSdTyN26D7h6dSpusAKaZkAiGM55/AKYi+9AGZw7q85hElbjK3kEyzXHhLSnRISHOYzVge6x0jRZ7DXA==", "dev": true, "license": "MIT", - "peer": true, "dependencies": { "@sveltejs/vite-plugin-svelte-inspector": "^5.0.0", "deepmerge": "^4.3.1", @@ -1963,7 +1960,6 @@ "integrity": "sha512-k4eNDan0EIMTT/dUKc/g+rsJ6wcHYhNPdY19VoX/EOtaAG8DLtKCykhrUnuHPYvinn5jhAPgD2Qw9hXBwrahsw==", "dev": true, "license": "MIT", - "peer": true, "dependencies": { "@typescript-eslint/scope-manager": "8.57.1", "@typescript-eslint/types": "8.57.1", @@ -2524,7 +2520,6 @@ "resolved": "https://registry.npmjs.org/acorn/-/acorn-8.15.0.tgz", "integrity": "sha512-NZyJarBfL7nWwIq+FDL6Zp/yHEhePMNnnJ0y3qfieCrmNvYct8uvtiV41UvlSe6apAfk0fY1FbWx+NwfmpvtTg==", "license": "MIT", - "peer": true, "bin": { "acorn": "bin/acorn" }, @@ -2811,7 +2806,6 @@ "integrity": "sha512-ChTCHMouEe2kn713WHbQGcuYrr6fXTBiu460OTwWrWob16g1bXn4vtz07Ope7ewMozJAnEquLk5lWQWtBig9DQ==", "dev": true, "license": "MIT", - "peer": true, "dependencies": { "follow-redirects": "^1.15.11", "form-data": "^4.0.5", @@ -2852,9 +2846,9 @@ } }, "node_modules/basic-ftp": { - "version": "5.2.0", - "resolved": "https://registry.npmjs.org/basic-ftp/-/basic-ftp-5.2.0.tgz", - "integrity": "sha512-VoMINM2rqJwJgfdHq6RiUudKt2BV+FY5ZFezP/ypmwayk68+NzzAQy4XXLlqsGD4MCzq3DrmNFD/uUmBJuGoXw==", + "version": "5.2.2", + "resolved": "https://registry.npmjs.org/basic-ftp/-/basic-ftp-5.2.2.tgz", + "integrity": "sha512-1tDrzKsdCg70WGvbFss/ulVAxupNauGnOlgpyjKzeQxzyllBLS0CGLV7tjIXTK3ZQA9/FBEm9qyFFN1bciA6pw==", "dev": true, "license": "MIT", "engines": { @@ -3805,7 +3799,6 @@ "integrity": "sha512-LEyamqS7W5HB3ujJyvi0HQK/dtVINZvd5mAAp9eT5S/ujByGjiZLCzPcHVzuXbpJDJF/cxwHlfceVUDZ2lnSTw==", "dev": true, "license": "MIT", - "peer": true, "dependencies": { "@eslint-community/eslint-utils": "^4.8.0", "@eslint-community/regexpp": "^4.12.1", @@ -3994,7 +3987,6 @@ "integrity": "sha512-whOE1HFo/qJDyX4SnXzP4N6zOWn79WhnCUY/iDR0mPfQZO8wcYE4JClzI2oZrhBnnMUCBCHZhO6VQyoBU95mZA==", "dev": true, "license": "MIT", - "peer": true, "dependencies": { "@rtsao/scc": "^1.1.0", "array-includes": "^3.1.9", @@ -6298,7 +6290,6 @@ } ], "license": "MIT", - "peer": true, "dependencies": { "nanoid": "^3.3.11", "picocolors": "^1.1.1", @@ -6634,8 +6625,7 @@ "resolved": "https://registry.npmjs.org/reflect-metadata/-/reflect-metadata-0.2.2.tgz", "integrity": "sha512-urBwgfrvVP/eAyXx4hluJivBKzuEbSQs9rKWCrCkbSxNv8mxPcUZKeuoF3Uy4mJl3Lwprp6yy5/39VWigZ4K6Q==", "dev": true, - "license": "Apache-2.0", - "peer": true + "license": "Apache-2.0" }, "node_modules/reflect.getprototypeof": { "version": "1.0.10", @@ -6823,7 +6813,6 @@ "integrity": "sha512-dhKf903U/PQZY6boNNtAGdWbG85WAbjT/1xYoZIC7FAY0yWapOBQVsVrDl58W86//e1VpMNBtRV4MaXfdMySFA==", "dev": true, "license": "Apache-2.0", - "peer": true, "dependencies": { "tslib": "^2.1.0" } @@ -6902,7 +6891,6 @@ "integrity": "sha512-AaIqGSrjo5lA2Yg7RvFZrlXDBCp3nV4XP73GrLGvdRWWwk+8H3l0SDvq/5bA4eF+0RFPLuWUk3E+P1U/YqnpsQ==", "dev": true, "license": "MIT", - "peer": true, "dependencies": { "chokidar": ">=3.0.0 <4.0.0", "immutable": "^4.0.0", @@ -7359,7 +7347,6 @@ "resolved": "https://registry.npmjs.org/svelte/-/svelte-5.54.0.tgz", "integrity": "sha512-TTDxwYnHkova6Wsyj1PGt9TByuWqvMoeY1bQiuAf2DM/JeDSMw7FjRKzk8K/5mJ99vGOKhbCqTDpyAKwjp4igg==", "license": "MIT", - "peer": true, "dependencies": { "@jridgewell/remapping": "^2.3.4", "@jridgewell/sourcemap-codec": "^1.5.0", @@ -7813,7 +7800,6 @@ "integrity": "sha512-jl1vZzPDinLr9eUt3J/t7V6FgNEw9QjvBPdysz9KfQDD41fQrC2Y4vKQdiaUpFT4bXlb1RHhLpp8wtm6M5TgSw==", "dev": true, "license": "Apache-2.0", - "peer": true, "bin": { "tsc": "bin/tsc", "tsserver": "bin/tsserver" @@ -7980,7 +7966,6 @@ "integrity": "sha512-EFrL7Hw4kmhZdwWO3dwwFJo6hO3FXuQ6Bg8BK/faHZ9m1YxqBS31BNSTxklIQkxK/4LlV8zTYnPsIRLBzTzjCA==", "dev": true, "hasInstallScript": true, - "peer": true, "dependencies": { "napi-postinstall": "^0.3.0" }, @@ -8032,7 +8017,6 @@ "integrity": "sha512-w+N7Hifpc3gRjZ63vYBXA56dvvRlNWRczTdmCBBa+CotUzAPf5b7YMdMR/8CQoeYE5LX3W4wj6RYTgonm1b9DA==", "dev": true, "license": "MIT", - "peer": true, "dependencies": { "esbuild": "^0.27.0", "fdir": "^6.5.0", From 0dbd8a3aa7e824e495b17c8effc8585e5fedc64a Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Sun, 12 Apr 2026 23:54:03 +0200 Subject: [PATCH 005/556] Bump docker/build-push-action from 7.0.0 to 7.1.0 (#1855) Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> --- .github/workflows/docker.yml | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/.github/workflows/docker.yml b/.github/workflows/docker.yml index ceeb973ee..f3aff007b 100644 --- a/.github/workflows/docker.yml +++ b/.github/workflows/docker.yml @@ -62,7 +62,7 @@ jobs: - name: Build Docker image without pushing if: github.event_name == 'pull_request' && github.event.pull_request.head.repo.full_name != github.repository id: build-no-push - uses: docker/build-push-action@d08e5c354a6adb9ed34480a06d141179aa583294 + uses: docker/build-push-action@bcafcacb16a39f128d818304e6c9c0c18556b85f with: file: docker/Dockerfile context: . @@ -74,7 +74,7 @@ jobs: - name: Build and push Docker image if: github.event_name != 'pull_request' || github.event.pull_request.head.repo.full_name == github.repository id: build - uses: docker/build-push-action@d08e5c354a6adb9ed34480a06d141179aa583294 + uses: docker/build-push-action@bcafcacb16a39f128d818304e6c9c0c18556b85f with: file: docker/Dockerfile context: . From 8d15619fa105f57fe28a7e0c0b596815f9e0e247 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Sun, 12 Apr 2026 23:54:20 +0200 Subject: [PATCH 006/556] Bump axios from 1.13.6 to 1.15.0 in /warpgate-web (#1857) Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> --- warpgate-web/package-lock.json | 18 ++++++++++++++---- 1 file changed, 14 insertions(+), 4 deletions(-) diff --git a/warpgate-web/package-lock.json b/warpgate-web/package-lock.json index 63675916d..163fa2697 100644 --- a/warpgate-web/package-lock.json +++ b/warpgate-web/package-lock.json @@ -2801,15 +2801,25 @@ } }, "node_modules/axios": { - "version": "1.13.6", - "resolved": "https://registry.npmjs.org/axios/-/axios-1.13.6.tgz", - "integrity": "sha512-ChTCHMouEe2kn713WHbQGcuYrr6fXTBiu460OTwWrWob16g1bXn4vtz07Ope7ewMozJAnEquLk5lWQWtBig9DQ==", + "version": "1.15.0", + "resolved": "https://registry.npmjs.org/axios/-/axios-1.15.0.tgz", + "integrity": "sha512-wWyJDlAatxk30ZJer+GeCWS209sA42X+N5jU2jy6oHTp7ufw8uzUTVFBX9+wTfAlhiJXGS0Bq7X6efruWjuK9Q==", "dev": true, "license": "MIT", "dependencies": { "follow-redirects": "^1.15.11", "form-data": "^4.0.5", - "proxy-from-env": "^1.1.0" + "proxy-from-env": "^2.1.0" + } + }, + "node_modules/axios/node_modules/proxy-from-env": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/proxy-from-env/-/proxy-from-env-2.1.0.tgz", + "integrity": "sha512-cJ+oHTW1VAEa8cJslgmUZrc+sjRKgAKl3Zyse6+PV38hZe/V6Z14TbCuXcan9F9ghlz4QrFr2c92TNF82UkYHA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=10" } }, "node_modules/axobject-query": { From e9c6aa3a8344f24dfcdd7a6ef932ee23081bd56e Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Sun, 12 Apr 2026 23:54:32 +0200 Subject: [PATCH 007/556] Bump rust from `bc19574` to `f4f82b8` in /docker (#1844) Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> --- docker/Dockerfile | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docker/Dockerfile b/docker/Dockerfile index c2da39d8f..36004ee27 100644 --- a/docker/Dockerfile +++ b/docker/Dockerfile @@ -1,6 +1,6 @@ # syntax=docker/dockerfile:1.3-labs # hadolint global ignore=DL3008 -FROM rust:1.94.1-bullseye@sha256:bc19574c121fe10c1bc68fc2b1ea9b420d87d047a0c50fb1622b282199700cee AS build +FROM rust:1.94.1-bullseye@sha256:f4f82b80e5f2945fed4ba17af177c6d6be85d98cde38ff318fc7666ce4505617 AS build ENV DEBIAN_FRONTEND=noninteractive From 8f52c5f39f6a8d8d0a9ad4cf999bdb4d1e0f6831 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Sun, 12 Apr 2026 23:54:35 +0200 Subject: [PATCH 008/556] Bump docker/login-action from 4.0.0 to 4.1.0 (#1834) Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> --- .github/workflows/docker.yml | 4 ++-- .github/workflows/helm-publish.yaml | 2 +- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/.github/workflows/docker.yml b/.github/workflows/docker.yml index f3aff007b..7b176e0f9 100644 --- a/.github/workflows/docker.yml +++ b/.github/workflows/docker.yml @@ -47,7 +47,7 @@ jobs: - name: Log into registry ${{ env.REGISTRY }} if: github.event_name != 'pull_request' || github.event.pull_request.head.repo.full_name == github.repository - uses: docker/login-action@b45d80f862d83dbcd57f89517bcf500b2ab88fb2 + uses: docker/login-action@4907a6ddec9925e35a0a9e82d7399ccc52663121 with: registry: ${{ env.REGISTRY }} username: ${{ github.actor }} @@ -127,7 +127,7 @@ jobs: uses: docker/setup-buildx-action@4d04d5d9486b7bd6fa91e7baf45bbb4f8b9deedd - name: Log into registry ${{ env.REGISTRY }} - uses: docker/login-action@b45d80f862d83dbcd57f89517bcf500b2ab88fb2 + uses: docker/login-action@4907a6ddec9925e35a0a9e82d7399ccc52663121 with: registry: ${{ env.REGISTRY }} username: ${{ github.actor }} diff --git a/.github/workflows/helm-publish.yaml b/.github/workflows/helm-publish.yaml index a4c5e23c4..d892c30c9 100644 --- a/.github/workflows/helm-publish.yaml +++ b/.github/workflows/helm-publish.yaml @@ -62,7 +62,7 @@ jobs: cat ${{ env.CHART_PATH }}/Chart.yaml - name: Log into registry ${{ env.REGISTRY }} - uses: docker/login-action@b45d80f862d83dbcd57f89517bcf500b2ab88fb2 + uses: docker/login-action@4907a6ddec9925e35a0a9e82d7399ccc52663121 with: registry: ${{ env.REGISTRY }} username: ${{ github.actor }} From 8a498eda575b7060a16876aeaeee50e288d95eb4 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Sun, 12 Apr 2026 23:54:47 +0200 Subject: [PATCH 009/556] Bump picomatch from 2.3.1 to 2.3.2 in /warpgate-web (#1823) Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> Co-authored-by: Eugene --- warpgate-web/package-lock.json | 18 +++++++++--------- 1 file changed, 9 insertions(+), 9 deletions(-) diff --git a/warpgate-web/package-lock.json b/warpgate-web/package-lock.json index 163fa2697..a9f5401c7 100644 --- a/warpgate-web/package-lock.json +++ b/warpgate-web/package-lock.json @@ -2604,9 +2604,9 @@ } }, "node_modules/anymatch/node_modules/picomatch": { - "version": "2.3.1", - "resolved": "https://registry.npmjs.org/picomatch/-/picomatch-2.3.1.tgz", - "integrity": "sha512-JU3teHTNjmE2VCGFzuY8EXzCDVwEqB2a8fsIvwaStHhAWJEeVd1o1QD80CU6+ZdEXXSLbSsuLwJjkCBWqRQUVA==", + "version": "2.3.2", + "resolved": "https://registry.npmjs.org/picomatch/-/picomatch-2.3.2.tgz", + "integrity": "sha512-V7+vQEJ06Z+c5tSye8S+nHUfI51xoXIXjHQ99cQtKUkQqqO1kO/KCJUfZXuB47h/YBlDhah2H3hdUGXn8ie0oA==", "dev": true, "license": "MIT", "engines": { @@ -6248,9 +6248,9 @@ "license": "ISC" }, "node_modules/picomatch": { - "version": "4.0.3", - "resolved": "https://registry.npmjs.org/picomatch/-/picomatch-4.0.3.tgz", - "integrity": "sha512-5gTmgEY/sqK6gFXLIsQNH19lWb4ebPDLA4SdLP7dsWkIXHWlG66oPuVvXSGFPppYZz8ZDZq0dYYrbHfBCVUb1Q==", + "version": "4.0.4", + "resolved": "https://registry.npmjs.org/picomatch/-/picomatch-4.0.4.tgz", + "integrity": "sha512-QP88BAKvMam/3NxH6vj2o21R6MjxZUAd6nlwAS/pnGvN9IVLocLHxGYIzFhg6fUQ+5th6P4dv4eW9jX3DSIj7A==", "dev": true, "license": "MIT", "engines": { @@ -6618,9 +6618,9 @@ } }, "node_modules/readdirp/node_modules/picomatch": { - "version": "2.3.1", - "resolved": "https://registry.npmjs.org/picomatch/-/picomatch-2.3.1.tgz", - "integrity": "sha512-JU3teHTNjmE2VCGFzuY8EXzCDVwEqB2a8fsIvwaStHhAWJEeVd1o1QD80CU6+ZdEXXSLbSsuLwJjkCBWqRQUVA==", + "version": "2.3.2", + "resolved": "https://registry.npmjs.org/picomatch/-/picomatch-2.3.2.tgz", + "integrity": "sha512-V7+vQEJ06Z+c5tSye8S+nHUfI51xoXIXjHQ99cQtKUkQqqO1kO/KCJUfZXuB47h/YBlDhah2H3hdUGXn8ie0oA==", "dev": true, "license": "MIT", "engines": { From 13bcc7f38a5046610d57bf84032aab1a2614ff9c Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Sun, 12 Apr 2026 23:54:54 +0200 Subject: [PATCH 010/556] Bump azure/setup-helm from 4 to 5 (#1820) Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> Co-authored-by: Eugene --- .github/workflows/helm-publish.yaml | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/.github/workflows/helm-publish.yaml b/.github/workflows/helm-publish.yaml index d892c30c9..6cad7d12e 100644 --- a/.github/workflows/helm-publish.yaml +++ b/.github/workflows/helm-publish.yaml @@ -23,7 +23,7 @@ jobs: uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd - name: Set up Helm - uses: azure/setup-helm@v4 + uses: azure/setup-helm@v5 - name: Lint chart run: helm lint ${{ env.CHART_PATH }} --strict @@ -45,7 +45,7 @@ jobs: uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd - name: Set up Helm - uses: azure/setup-helm@v4 + uses: azure/setup-helm@v5 - name: Extract version from tag id: version From 0503707a9cbade744fa21d10288730d2bc89ec9a Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Sun, 12 Apr 2026 23:55:01 +0200 Subject: [PATCH 011/556] Bump SonarSource/sonarqube-scan-action from 7.0.0 to 7.1.0 (#1833) Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> --- .github/workflows/test.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/test.yml b/.github/workflows/test.yml index c8fc3ebfa..9d71bfa48 100644 --- a/.github/workflows/test.yml +++ b/.github/workflows/test.yml @@ -70,7 +70,7 @@ jobs: path: target/llvm-cov/html - name: SonarCloud Scan - uses: SonarSource/sonarqube-scan-action@a31c9398be7ace6bbfaf30c0bd5d415f843d45e9 + uses: SonarSource/sonarqube-scan-action@299e4b793aaa83bf2aba7c9c14bedbb485688ec4 if: ${{ env.SONAR_TOKEN }} env: GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} # Needed to get PR information, if any From e98578815938c327f58f1f190e1c83ef9d848800 Mon Sep 17 00:00:00 2001 From: Eugene Date: Mon, 13 Apr 2026 08:57:39 +0200 Subject: [PATCH 012/556] replace ansi_term --- Cargo.lock | 29 +++++++++---------- deny.toml | 12 ++++---- warpgate-protocol-http/src/common.rs | 5 +++- warpgate-protocol-ssh/Cargo.toml | 2 +- .../src/server/service_output.rs | 13 +++++++-- warpgate-protocol-ssh/src/server/session.rs | 20 ++++++------- warpgate/Cargo.toml | 1 - 7 files changed, 46 insertions(+), 36 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index 7f4e34590..1fda54a72 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -119,15 +119,6 @@ dependencies = [ "libc", ] -[[package]] -name = "ansi_term" -version = "0.12.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d52a9bb7ec0cf484c551830a7ce27bd20d67eac647e1befb56b0be4ee39a55d2" -dependencies = [ - "winapi", -] - [[package]] name = "anstream" version = "1.0.0" @@ -1683,9 +1674,9 @@ checksum = "4443176a9f2c162692bd3d352d745ef9413eec5782a80d8fd6f8a1ac692a07f7" [[package]] name = "fastrand" -version = "2.4.0" +version = "2.4.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a043dc74da1e37d6afe657061213aa6f425f855399a11d3463c6ecccc4dfda1f" +checksum = "9f1f227452a390804cdb637b74a86990f2a7d7ba4b7d5693aac9b4dd6defd8d6" [[package]] name = "ff" @@ -2896,9 +2887,9 @@ dependencies = [ [[package]] name = "libz-sys" -version = "1.1.26" +version = "1.1.28" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "786a7c68b5bbe177567d237ec4940d11666206e97b20a983421b251092f24d7d" +checksum = "fc3a226e576f50782b3305c5ccf458698f92798987f551c6a02efe8276721e22" dependencies = [ "cc", "pkg-config", @@ -5905,6 +5896,15 @@ dependencies = [ "windows-sys 0.61.2", ] +[[package]] +name = "termcolor" +version = "1.4.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "06794f8f6c5c898b3275aebefa6b8a1cb24cd2c6c79397ab15774837a0bc5755" +dependencies = [ + "winapi-util", +] + [[package]] name = "thiserror" version = "1.0.69" @@ -6554,7 +6554,6 @@ dependencies = [ name = "warpgate" version = "0.22.0-beta.5" dependencies = [ - "ansi_term", "anyhow", "async-trait", "bytes", @@ -6955,7 +6954,6 @@ dependencies = [ name = "warpgate-protocol-ssh" version = "0.22.0-beta.5" dependencies = [ - "ansi_term", "anyhow", "async-trait", "bimap", @@ -6967,6 +6965,7 @@ dependencies = [ "russh", "sea-orm", "serde", + "termcolor", "thiserror 2.0.18", "time", "tokio", diff --git a/deny.toml b/deny.toml index 889b3354b..4134172a1 100644 --- a/deny.toml +++ b/deny.toml @@ -67,9 +67,9 @@ feature-depth = 1 # A list of advisory IDs to ignore. Note that ignored advisories will still # output a note when they are encountered. ignore = [ - "RUSTSEC-2023-0071", - "RUSTSEC-2021-0139", # ansi-term is unmaintained + "RUSTSEC-2023-0071", # Marvin Attack: potential key recovery through timing sidechannels "RUSTSEC-2025-0134", # rustls-pemfile is deprecated but poem is still using it + "RUSTSEC-2026-0097", # Rand is unsound with a custom logger using `rand::rng()` ] # If this is true, then cargo deny will use the git executable to fetch advisory database. # If this is false, then it uses a built-in git library. @@ -83,7 +83,7 @@ ignore = [ # https://embarkstudios.github.io/cargo-deny/checks/bans/cfg.html [bans] # Lint level for when multiple versions of the same crate are detected -# multiple-versions = "warn" +# multiple-versions = "deny" # Lint level for when a crate version requirement is `*` wildcards = "warn" # The graph highlighting used when creating dotgraphs for crates @@ -95,7 +95,7 @@ highlight = "all" # The default lint level for `default` features for crates that are members of # the workspace that is being checked. This can be overridden by allowing/denying # `default` on a crate-by-crate basis if desired. -workspace-default-features = "warn" +workspace-default-features = "allow" # The default lint level for `default` features for external crates that are not # members of the workspace. This can be overridden by allowing/denying `default` # on a crate-by-crate basis if desired. @@ -164,10 +164,10 @@ deny = ["rustls-tls-webpki-roots"] [sources] # Lint level for what to happen when a crate from a crate registry that is not # in the allow list is encountered -unknown-registry = "warn" +unknown-registry = "deny" # Lint level for what to happen when a crate from a git repository that is not # in the allow list is encountered -unknown-git = "warn" +unknown-git = "deny" # List of URLs for allowed crate registries. Defaults to the crates.io index # if not specified. If it is specified but empty, no registries are allowed. allow-registry = ["https://github.com/rust-lang/crates.io-index"] diff --git a/warpgate-protocol-http/src/common.rs b/warpgate-protocol-http/src/common.rs index 49e208a78..fc9a5c9a8 100644 --- a/warpgate-protocol-http/src/common.rs +++ b/warpgate-protocol-http/src/common.rs @@ -300,7 +300,10 @@ pub async fn inject_request_authorization( .as_deref() .is_some_and(|admin_token| { // Use constant time comparison to prevent timing attacks - admin_token.as_bytes().ct_eq(token_from_header.as_bytes()).into() + admin_token + .as_bytes() + .ct_eq(token_from_header.as_bytes()) + .into() }) { Some(RequestAuthorization::AdminToken) diff --git a/warpgate-protocol-ssh/Cargo.toml b/warpgate-protocol-ssh/Cargo.toml index 8579098f0..b6e21e8ba 100644 --- a/warpgate-protocol-ssh/Cargo.toml +++ b/warpgate-protocol-ssh/Cargo.toml @@ -6,7 +6,7 @@ version = "0.22.0-beta.5" publish = false [dependencies] -ansi_term = { version = "0.12", default-features = false } +termcolor = { version = "1", default-features = false } anyhow.workspace = true async-trait = { version = "0.1", default-features = false } bimap = { version = "0.6", default-features = false, features = ["std"] } diff --git a/warpgate-protocol-ssh/src/server/service_output.rs b/warpgate-protocol-ssh/src/server/service_output.rs index 759373d9e..e58d281b0 100644 --- a/warpgate-protocol-ssh/src/server/service_output.rs +++ b/warpgate-protocol-ssh/src/server/service_output.rs @@ -1,13 +1,22 @@ +use std::io::Write as _; use std::sync::atomic::AtomicBool; use std::sync::Arc; -use ansi_term::Colour; use bytes::Bytes; +use termcolor::{Buffer, Color, ColorSpec, WriteColor as _}; use tokio::sync::{broadcast, mpsc}; pub const ERASE_PROGRESS_SPINNER: &str = "\r \r"; pub const ERASE_PROGRESS_SPINNER_BUF: &[u8] = ERASE_PROGRESS_SPINNER.as_bytes(); +pub(super) fn ansi_paint(fg: Color, bg: Color, text: &str) -> String { + let mut buf = Buffer::ansi(); + let _ = buf.set_color(ColorSpec::new().set_fg(Some(fg)).set_bg(Some(bg))); + let _ = write!(buf, "{text}"); + let _ = buf.reset(); + String::from_utf8_lossy(buf.as_slice()).to_string() +} + #[derive(Clone)] pub struct ServiceOutput { progress_visible: Arc, @@ -37,7 +46,7 @@ impl ServiceOutput { tick_index = (tick_index + 1) % ticks.len(); #[allow(clippy::indexing_slicing)] let tick = ticks[tick_index]; - let badge = Colour::Black.on(Colour::Blue).paint(format!(" {tick} Warpgate connecting ")).to_string(); + let badge = ansi_paint(Color::Black, Color::Blue, &format!(" {tick} Warpgate connecting ")); let _ = output_tx.send(Bytes::from([ERASE_PROGRESS_SPINNER_BUF, badge.as_bytes()].concat())); } } diff --git a/warpgate-protocol-ssh/src/server/session.rs b/warpgate-protocol-ssh/src/server/session.rs index f61c830e6..4b55ff1d2 100644 --- a/warpgate-protocol-ssh/src/server/session.rs +++ b/warpgate-protocol-ssh/src/server/session.rs @@ -7,13 +7,13 @@ use std::str::FromStr; use std::sync::Arc; use std::task::Poll; -use ansi_term::Colour; use anyhow::{Context, Result}; use bimap::BiMap; use bytes::Bytes; use futures::{Future, FutureExt}; use russh::keys::{PublicKey, PublicKeyBase64}; use russh::{MethodKind, MethodSet, Sig}; +use termcolor::Color; use tokio::sync::mpsc::{UnboundedReceiver, UnboundedSender}; use tokio::sync::{broadcast, oneshot, Mutex}; use tracing::*; @@ -36,7 +36,7 @@ use warpgate_core::{ use super::channel_writer::ChannelWriter; use super::russh_handler::ServerHandlerEvent; -use super::service_output::ServiceOutput; +use super::service_output::{ansi_paint, ServiceOutput}; use super::session_handle::SessionHandleCommand; use crate::compat::ContextExt; use crate::server::get_allowed_auth_methods; @@ -306,7 +306,7 @@ impl ServerSession { let output = format!( "{}{} {}\r\n", ERASE_PROGRESS_SPINNER, - Colour::Black.on(Colour::White).paint(" Warpgate "), + ansi_paint(Color::Black, Color::White, " Warpgate "), msg.replace('\n', "\r\n"), ); self.emit_pty_output(output.as_bytes())?; @@ -631,9 +631,7 @@ impl ServerSession { let msg = format!( "{}{}\r\n", ERASE_PROGRESS_SPINNER, - Colour::Black - .on(Colour::Green) - .paint(" ✓ Warpgate connected ") + ansi_paint(Color::Black, Color::Green, " ✓ Warpgate connected ") ); let _ = self.emit_pty_output(msg.as_bytes()); } @@ -678,9 +676,11 @@ impl ServerSession { let msg = format!( "{}{}\r\n", ERASE_PROGRESS_SPINNER, - Colour::Black - .on(Colour::Red) - .paint(" ✗ SSH target rejected Warpgate authentication request ") + ansi_paint( + Color::Black, + Color::Red, + " ✗ SSH target rejected Warpgate authentication request " + ) ); let _ = self.emit_pty_output(msg.as_bytes()); } @@ -688,7 +688,7 @@ impl ServerSession { let msg = format!( "{}{} {}\r\n", ERASE_PROGRESS_SPINNER, - Colour::Black.on(Colour::Red).paint(" ✗ Connection failed "), + ansi_paint(Color::Black, Color::Red, " ✗ Connection failed "), error ); let _ = self.emit_pty_output(msg.as_bytes()); diff --git a/warpgate/Cargo.toml b/warpgate/Cargo.toml index 0404d0c7f..197654134 100644 --- a/warpgate/Cargo.toml +++ b/warpgate/Cargo.toml @@ -6,7 +6,6 @@ version = "0.22.0-beta.5" publish = false [dependencies] -ansi_term = { version = "0.12", default-features = false } anyhow.workspace = true async-trait = { version = "0.1", default-features = false } bytes.workspace = true From 984c47e8f782ea59417c3b68c43b4905e3d0055f Mon Sep 17 00:00:00 2001 From: Eugene Date: Mon, 13 Apr 2026 10:30:42 +0200 Subject: [PATCH 013/556] IAM auth (#1859) --- .github/workflows/build.yml | 6 - .github/workflows/cargo-deny.yml | 20 + Cargo.lock | 743 ++++++++++++++++-- Cargo.toml | 21 +- tests/test_postgres_user_auth_in_browser.py | 25 +- tests/test_postgres_user_auth_password.py | 35 +- warpgate-admin/src/api/sessions_list.rs | 1 + warpgate-admin/src/api/targets.rs | 16 +- warpgate-aws/Cargo.toml | 41 + warpgate-aws/src/ec2.rs | 170 ++++ warpgate-aws/src/eks.rs | 128 +++ warpgate-aws/src/error.rs | 38 + warpgate-aws/src/lib.rs | 44 ++ warpgate-aws/src/rds.rs | 37 + warpgate-aws/src/region.rs | 108 +++ warpgate-common/Cargo.toml | 1 + warpgate-common/src/config/target.rs | 102 ++- warpgate-common/src/error.rs | 9 + warpgate-db-entities/src/Target.rs | 9 - warpgate-db-migrations/src/lib.rs | 4 + .../src/m00037_database_target_auth.rs | 115 +++ .../src/m00038_fix_target_auth_tags.rs | 121 +++ warpgate-protocol-http/Cargo.toml | 1 + warpgate-protocol-http/src/api/info.rs | 6 + warpgate-protocol-kubernetes/Cargo.toml | 1 + .../src/server/auth.rs | 22 +- .../src/server/handlers.rs | 7 +- warpgate-protocol-mysql/Cargo.toml | 1 + warpgate-protocol-mysql/src/client.rs | 21 +- warpgate-protocol-postgres/Cargo.toml | 1 + warpgate-protocol-postgres/src/client.rs | 40 +- warpgate-protocol-postgres/src/error.rs | 2 - warpgate-protocol-ssh/Cargo.toml | 1 + warpgate-protocol-ssh/src/client/mod.rs | 50 +- warpgate-protocol-ssh/src/keys.rs | 9 + .../admin/config/targets/CreateTarget.svelte | 8 +- .../src/admin/config/targets/Target.svelte | 20 +- .../admin/config/targets/ssh/Options.svelte | 4 + .../src/admin/lib/openapi-schema.json | 151 +++- .../src/gateway/lib/openapi-schema.json | 5 +- warpgate/src/commands/setup.rs | 29 + 41 files changed, 2012 insertions(+), 161 deletions(-) create mode 100644 .github/workflows/cargo-deny.yml create mode 100644 warpgate-aws/Cargo.toml create mode 100644 warpgate-aws/src/ec2.rs create mode 100644 warpgate-aws/src/eks.rs create mode 100644 warpgate-aws/src/error.rs create mode 100644 warpgate-aws/src/lib.rs create mode 100644 warpgate-aws/src/rds.rs create mode 100644 warpgate-aws/src/region.rs create mode 100644 warpgate-db-migrations/src/m00037_database_target_auth.rs create mode 100644 warpgate-db-migrations/src/m00038_fix_target_auth_tags.rs diff --git a/.github/workflows/build.yml b/.github/workflows/build.yml index b654f5939..17cedc7e0 100644 --- a/.github/workflows/build.yml +++ b/.github/workflows/build.yml @@ -67,7 +67,6 @@ jobs: - name: Install tools run: | cargo install --locked just - cargo install --locked cargo-deny@0.18.9 cargo install --locked cargo-cyclonedx@^0.5 rm -rf ~/.cargo/registry @@ -82,11 +81,6 @@ jobs: cd / npm i -g @cyclonedx/cyclonedx-npm@4.1.2 - - name: cargo-deny - run: | - cargo deny --version - cargo deny check - - name: Install admin UI deps run: | just npm ci diff --git a/.github/workflows/cargo-deny.yml b/.github/workflows/cargo-deny.yml new file mode 100644 index 000000000..6239ff1c1 --- /dev/null +++ b/.github/workflows/cargo-deny.yml @@ -0,0 +1,20 @@ +name: Cargo Deny +permissions: + contents: read + +on: [push, pull_request] + +jobs: + cargo-deny: + name: cargo-deny + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd + + - name: Install cargo-deny + run: cargo install --locked cargo-deny@0.18.9 + + - name: cargo-deny check + run: | + cargo deny --version + cargo deny check diff --git a/Cargo.lock b/Cargo.lock index 1fda54a72..d35f5025a 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -354,6 +354,43 @@ version = "1.5.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "c08606f8c3cbf4ce6ec8e28fb0014a2c086708fe954eaa885384a6165172e7e8" +[[package]] +name = "aws-config" +version = "1.8.15" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "11493b0bad143270fb8ad284a096dd529ba91924c5409adeac856cc1bf047dbc" +dependencies = [ + "aws-credential-types", + "aws-runtime", + "aws-sdk-sts", + "aws-smithy-async", + "aws-smithy-http", + "aws-smithy-json", + "aws-smithy-runtime", + "aws-smithy-runtime-api", + "aws-smithy-types", + "aws-types", + "bytes", + "fastrand", + "http 1.4.0", + "time", + "tokio", + "tracing", + "url", +] + +[[package]] +name = "aws-credential-types" +version = "1.2.14" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8f20799b373a1be121fe3005fba0c2090af9411573878f224df44b42727fcaf7" +dependencies = [ + "aws-smithy-async", + "aws-smithy-runtime-api", + "aws-smithy-types", + "zeroize", +] + [[package]] name = "aws-lc-rs" version = "1.16.2" @@ -377,6 +414,359 @@ dependencies = [ "fs_extra", ] +[[package]] +name = "aws-runtime" +version = "1.7.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5fc0651c57e384202e47153c1260b84a9936e19803d747615edf199dc3b98d17" +dependencies = [ + "aws-credential-types", + "aws-sigv4", + "aws-smithy-async", + "aws-smithy-http", + "aws-smithy-runtime", + "aws-smithy-runtime-api", + "aws-smithy-types", + "aws-types", + "bytes", + "bytes-utils", + "fastrand", + "http 1.4.0", + "http-body 1.0.1", + "percent-encoding", + "pin-project-lite", + "tracing", + "uuid", +] + +[[package]] +name = "aws-sdk-ec2" +version = "1.220.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1065222c6fe7bed0ef49acf2bfdba8ab9b59cc14bc534772d575b365601cd557" +dependencies = [ + "aws-credential-types", + "aws-runtime", + "aws-smithy-async", + "aws-smithy-http", + "aws-smithy-json", + "aws-smithy-observability", + "aws-smithy-query", + "aws-smithy-runtime", + "aws-smithy-runtime-api", + "aws-smithy-types", + "aws-smithy-xml", + "aws-types", + "fastrand", + "http 0.2.12", + "http 1.4.0", + "regex-lite", + "tracing", +] + +[[package]] +name = "aws-sdk-ec2instanceconnect" +version = "1.97.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e6fb2758259cc6b8bec31d864ecc5d02967f8683eb37cbb5500339df7f108d14" +dependencies = [ + "aws-credential-types", + "aws-runtime", + "aws-smithy-async", + "aws-smithy-http", + "aws-smithy-json", + "aws-smithy-observability", + "aws-smithy-runtime", + "aws-smithy-runtime-api", + "aws-smithy-types", + "aws-types", + "bytes", + "fastrand", + "http 0.2.12", + "http 1.4.0", + "regex-lite", + "tracing", +] + +[[package]] +name = "aws-sdk-eks" +version = "1.127.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a6bdb45d08733dc9ab5bf2966d3a0716b58f1347d7e622971f266cf294b4fdc1" +dependencies = [ + "aws-credential-types", + "aws-runtime", + "aws-smithy-async", + "aws-smithy-http", + "aws-smithy-json", + "aws-smithy-observability", + "aws-smithy-runtime", + "aws-smithy-runtime-api", + "aws-smithy-types", + "aws-types", + "bytes", + "fastrand", + "http 0.2.12", + "http 1.4.0", + "regex-lite", + "tracing", +] + +[[package]] +name = "aws-sdk-rds" +version = "1.130.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "adf504300ce52abfe00276b1496d72ce762a2fc9b20908ae24db33905c1da51a" +dependencies = [ + "aws-credential-types", + "aws-runtime", + "aws-sigv4", + "aws-smithy-async", + "aws-smithy-http", + "aws-smithy-json", + "aws-smithy-observability", + "aws-smithy-query", + "aws-smithy-runtime", + "aws-smithy-runtime-api", + "aws-smithy-types", + "aws-smithy-xml", + "aws-types", + "fastrand", + "http 0.2.12", + "http 1.4.0", + "regex-lite", + "tracing", + "url", +] + +[[package]] +name = "aws-sdk-sts" +version = "1.101.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ab41ad64e4051ecabeea802d6a17845a91e83287e1dd249e6963ea1ba78c428a" +dependencies = [ + "aws-credential-types", + "aws-runtime", + "aws-smithy-async", + "aws-smithy-http", + "aws-smithy-json", + "aws-smithy-observability", + "aws-smithy-query", + "aws-smithy-runtime", + "aws-smithy-runtime-api", + "aws-smithy-types", + "aws-smithy-xml", + "aws-types", + "fastrand", + "http 0.2.12", + "http 1.4.0", + "regex-lite", + "tracing", +] + +[[package]] +name = "aws-sigv4" +version = "1.4.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b0b660013a6683ab23797778e21f1f854744fdf05f68204b4cca4c8c04b5d1f4" +dependencies = [ + "aws-credential-types", + "aws-smithy-http", + "aws-smithy-runtime-api", + "aws-smithy-types", + "bytes", + "form_urlencoded", + "hex", + "hmac 0.12.1", + "http 0.2.12", + "http 1.4.0", + "percent-encoding", + "sha2 0.10.9", + "time", + "tracing", +] + +[[package]] +name = "aws-smithy-async" +version = "1.2.14" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2ffcaf626bdda484571968400c326a244598634dc75fd451325a54ad1a59acfc" +dependencies = [ + "futures-util", + "pin-project-lite", + "tokio", +] + +[[package]] +name = "aws-smithy-http" +version = "0.63.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ba1ab2dc1c2c3749ead27180d333c42f11be8b0e934058fb4b2258ee8dbe5231" +dependencies = [ + "aws-smithy-runtime-api", + "aws-smithy-types", + "bytes", + "bytes-utils", + "futures-core", + "futures-util", + "http 1.4.0", + "http-body 1.0.1", + "http-body-util", + "percent-encoding", + "pin-project-lite", + "pin-utils", + "tracing", +] + +[[package]] +name = "aws-smithy-http-client" +version = "1.1.12" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6a2f165a7feee6f263028b899d0a181987f4fa7179a6411a32a439fba7c5f769" +dependencies = [ + "aws-smithy-async", + "aws-smithy-runtime-api", + "aws-smithy-types", + "h2 0.3.27", + "h2 0.4.13", + "http 0.2.12", + "http 1.4.0", + "http-body 0.4.6", + "hyper 0.14.32", + "hyper 1.9.0", + "hyper-rustls 0.24.2", + "hyper-rustls 0.27.7", + "hyper-util", + "pin-project-lite", + "rustls 0.21.12", + "rustls 0.23.37", + "rustls-native-certs", + "rustls-pki-types", + "tokio", + "tokio-rustls 0.26.4", + "tower 0.5.3", + "tracing", +] + +[[package]] +name = "aws-smithy-json" +version = "0.62.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9648b0bb82a2eedd844052c6ad2a1a822d1f8e3adee5fbf668366717e428856a" +dependencies = [ + "aws-smithy-types", +] + +[[package]] +name = "aws-smithy-observability" +version = "0.2.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a06c2315d173edbf1920da8ba3a7189695827002e4c0fc961973ab1c54abca9c" +dependencies = [ + "aws-smithy-runtime-api", +] + +[[package]] +name = "aws-smithy-query" +version = "0.60.15" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1a56d79744fb3edb5d722ef79d86081e121d3b9422cb209eb03aea6aa4f21ebd" +dependencies = [ + "aws-smithy-types", + "urlencoding", +] + +[[package]] +name = "aws-smithy-runtime" +version = "1.10.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "028999056d2d2fd58a697232f9eec4a643cf73a71cf327690a7edad1d2af2110" +dependencies = [ + "aws-smithy-async", + "aws-smithy-http", + "aws-smithy-http-client", + "aws-smithy-observability", + "aws-smithy-runtime-api", + "aws-smithy-types", + "bytes", + "fastrand", + "http 0.2.12", + "http 1.4.0", + "http-body 0.4.6", + "http-body 1.0.1", + "http-body-util", + "pin-project-lite", + "pin-utils", + "tokio", + "tracing", +] + +[[package]] +name = "aws-smithy-runtime-api" +version = "1.11.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "876ab3c9c29791ba4ba02b780a3049e21ec63dabda09268b175272c3733a79e6" +dependencies = [ + "aws-smithy-async", + "aws-smithy-types", + "bytes", + "http 0.2.12", + "http 1.4.0", + "pin-project-lite", + "tokio", + "tracing", + "zeroize", +] + +[[package]] +name = "aws-smithy-types" +version = "1.4.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9d73dbfbaa8e4bc57b9045137680b958d274823509a360abfd8e1d514d40c95c" +dependencies = [ + "base64-simd", + "bytes", + "bytes-utils", + "futures-core", + "http 0.2.12", + "http 1.4.0", + "http-body 0.4.6", + "http-body 1.0.1", + "http-body-util", + "itoa", + "num-integer", + "pin-project-lite", + "pin-utils", + "ryu", + "serde", + "time", + "tokio", + "tokio-util", +] + +[[package]] +name = "aws-smithy-xml" +version = "0.60.15" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0ce02add1aa3677d022f8adf81dcbe3046a95f17a1b1e8979c145cd21d3d22b3" +dependencies = [ + "xmlparser", +] + +[[package]] +name = "aws-types" +version = "1.3.14" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "47c8323699dd9b3c8d5b3c13051ae9cdef58fd179957c882f8374dd8725962d9" +dependencies = [ + "aws-credential-types", + "aws-smithy-async", + "aws-smithy-runtime-api", + "aws-smithy-types", + "rustc_version", + "tracing", +] + [[package]] name = "axum" version = "0.7.9" @@ -387,8 +777,8 @@ dependencies = [ "axum-core", "bytes", "futures-util", - "http", - "http-body", + "http 1.4.0", + "http-body 1.0.1", "http-body-util", "itoa", "matchit", @@ -413,8 +803,8 @@ dependencies = [ "async-trait", "bytes", "futures-util", - "http", - "http-body", + "http 1.4.0", + "http-body 1.0.1", "http-body-util", "mime", "pin-project-lite", @@ -454,6 +844,16 @@ version = "0.22.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "72b3254f16251a8381aa12e40e3c4d2f0199f8c6508fbecb9d91f575e0fbb8c6" +[[package]] +name = "base64-simd" +version = "0.8.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "339abbe78e73178762e23bea9dfd08e697eb3f3301cd4be981c0f78ba5859195" +dependencies = [ + "outref", + "vsimd", +] + [[package]] name = "base64ct" version = "1.8.3" @@ -646,6 +1046,16 @@ version = "1.11.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "1e748733b7cbc798e1434b6ac524f0c1ff2ab456fe201501e6497c8417a4fc33" +[[package]] +name = "bytes-utils" +version = "0.1.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7dafe3a8757b027e2be6e4e5601ed563c55989fcf1546e933c66c8eb3a058d35" +dependencies = [ + "bytes", + "either", +] + [[package]] name = "cbc" version = "0.1.2" @@ -2028,6 +2438,25 @@ dependencies = [ "subtle", ] +[[package]] +name = "h2" +version = "0.3.27" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0beca50380b1fc32983fc1cb4587bfa4bb9e78fc259aad4a0032d2080309222d" +dependencies = [ + "bytes", + "fnv", + "futures-core", + "futures-sink", + "futures-util", + "http 0.2.12", + "indexmap 2.13.1", + "slab", + "tokio", + "tokio-util", + "tracing", +] + [[package]] name = "h2" version = "0.4.13" @@ -2039,7 +2468,7 @@ dependencies = [ "fnv", "futures-core", "futures-sink", - "http", + "http 1.4.0", "indexmap 2.13.1", "slab", "tokio", @@ -2115,7 +2544,7 @@ dependencies = [ "base64 0.22.1", "bytes", "headers-core", - "http", + "http 1.4.0", "httpdate", "mime", "sha1 0.10.6", @@ -2127,7 +2556,7 @@ version = "0.3.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "54b4a22553d4242c49fddb9ba998a99962b5cc6f22cb5a3482bec22522403ce4" dependencies = [ - "http", + "http 1.4.0", ] [[package]] @@ -2199,6 +2628,17 @@ dependencies = [ "windows-sys 0.61.2", ] +[[package]] +name = "http" +version = "0.2.12" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "601cbb57e577e2f5ef5be8e7b83f0f63994f25aa94d673e54a92d5c516d101f1" +dependencies = [ + "bytes", + "fnv", + "itoa", +] + [[package]] name = "http" version = "1.4.0" @@ -2209,6 +2649,17 @@ dependencies = [ "itoa", ] +[[package]] +name = "http-body" +version = "0.4.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7ceab25649e9960c0311ea418d17bee82c0dcec1bd053b5f9a66e265a693bed2" +dependencies = [ + "bytes", + "http 0.2.12", + "pin-project-lite", +] + [[package]] name = "http-body" version = "1.0.1" @@ -2216,7 +2667,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "1efedce1fb8e6913f23e0c92de8e62cd5b772a67e7b3946df930a62566c93184" dependencies = [ "bytes", - "http", + "http 1.4.0", ] [[package]] @@ -2227,8 +2678,8 @@ checksum = "b021d93e26becf5dc7e1b75b1bed1fd93124b374ceb73f43d4d4eafec896a64a" dependencies = [ "bytes", "futures-core", - "http", - "http-body", + "http 1.4.0", + "http-body 1.0.1", "pin-project-lite", ] @@ -2281,6 +2732,30 @@ dependencies = [ "zeroize", ] +[[package]] +name = "hyper" +version = "0.14.32" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "41dfc780fdec9373c01bae43289ea34c972e40ee3c9f6b3c8801a35f35586ce7" +dependencies = [ + "bytes", + "futures-channel", + "futures-core", + "futures-util", + "h2 0.3.27", + "http 0.2.12", + "http-body 0.4.6", + "httparse", + "httpdate", + "itoa", + "pin-project-lite", + "socket2 0.5.10", + "tokio", + "tower-service", + "tracing", + "want", +] + [[package]] name = "hyper" version = "1.9.0" @@ -2291,9 +2766,9 @@ dependencies = [ "bytes", "futures-channel", "futures-core", - "h2", - "http", - "http-body", + "h2 0.4.13", + "http 1.4.0", + "http-body 1.0.1", "httparse", "httpdate", "itoa", @@ -2303,20 +2778,35 @@ dependencies = [ "want", ] +[[package]] +name = "hyper-rustls" +version = "0.24.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ec3efd23720e2049821a693cbc7e65ea87c72f1c58ff2f9522ff332b1491e590" +dependencies = [ + "futures-util", + "http 0.2.12", + "hyper 0.14.32", + "log", + "rustls 0.21.12", + "tokio", + "tokio-rustls 0.24.1", +] + [[package]] name = "hyper-rustls" version = "0.27.7" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "e3c93eb611681b207e1fe55d5a71ecf91572ec8a6705cdb6857f7d8d5242cf58" dependencies = [ - "http", - "hyper", + "http 1.4.0", + "hyper 1.9.0", "hyper-util", - "rustls", + "rustls 0.23.37", "rustls-native-certs", "rustls-pki-types", "tokio", - "tokio-rustls", + "tokio-rustls 0.26.4", "tower-service", ] @@ -2326,7 +2816,7 @@ version = "0.5.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "2b90d566bffbce6a75bd8b09a05aa8c2cb1fabb6cb348f8840c9e4c90a0d83b0" dependencies = [ - "hyper", + "hyper 1.9.0", "hyper-util", "pin-project-lite", "tokio", @@ -2343,9 +2833,9 @@ dependencies = [ "bytes", "futures-channel", "futures-util", - "http", - "http-body", - "hyper", + "http 1.4.0", + "http-body 1.0.1", + "hyper 1.9.0", "ipnet", "libc", "percent-encoding", @@ -2833,11 +3323,11 @@ dependencies = [ "log", "nom", "percent-encoding", - "rustls", + "rustls 0.23.37", "rustls-native-certs", "thiserror 2.0.18", "tokio", - "tokio-rustls", + "tokio-rustls 0.26.4", "tokio-stream", "tokio-util", "url", @@ -3067,7 +3557,7 @@ dependencies = [ "bytes", "encoding_rs", "futures-util", - "http", + "http 1.4.0", "httparse", "memchr", "mime", @@ -3277,7 +3767,7 @@ dependencies = [ "base64 0.22.1", "chrono", "getrandom 0.2.17", - "http", + "http 1.4.0", "rand 0.8.5", "reqwest 0.12.28", "serde", @@ -3335,7 +3825,7 @@ dependencies = [ "dyn-clone", "ed25519-dalek 2.2.0", "hmac 0.12.1", - "http", + "http 1.4.0", "itertools 0.10.5", "log", "oauth2", @@ -3403,6 +3893,12 @@ dependencies = [ "syn 2.0.117", ] +[[package]] +name = "outref" +version = "0.5.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1a80800c0488c3a21695ea981a54918fbb37abf04f4d0720c453632255e2ff0e" + [[package]] name = "p256" version = "0.13.2" @@ -3629,7 +4125,7 @@ dependencies = [ "rustls-pki-types", "thiserror 2.0.18", "tokio", - "tokio-rustls", + "tokio-rustls 0.26.4", "tokio-util", ] @@ -3677,6 +4173,12 @@ version = "0.2.17" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "a89322df9ebe1c1578d689c92318e070967d1042b512afbe49518723f4e6d5cd" +[[package]] +name = "pin-utils" +version = "0.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8b870d8c151b6f2fb93e84a13146138f05d02ed11c7e7c54f8826aaaf7c9f184" + [[package]] name = "pkcs1" version = "0.7.5" @@ -3763,10 +4265,10 @@ dependencies = [ "futures-util", "headers", "hex", - "http", + "http 1.4.0", "http-body-util", "httpdate", - "hyper", + "hyper 1.9.0", "hyper-util", "mime", "mime_guess", @@ -3793,7 +4295,7 @@ dependencies = [ "thiserror 2.0.18", "time", "tokio", - "tokio-rustls", + "tokio-rustls 0.26.4", "tokio-stream", "tokio-tungstenite", "tokio-util", @@ -3848,7 +4350,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "41273b691a3d467a8c44d05506afba9f7b6bd56c9cdf80123de13fe52d7ec587" dependencies = [ "darling 0.20.11", - "http", + "http 1.4.0", "indexmap 2.13.1", "mime", "proc-macro-crate", @@ -4366,18 +4868,18 @@ dependencies = [ "bytes", "futures-core", "futures-util", - "h2", - "http", - "http-body", + "h2 0.4.13", + "http 1.4.0", + "http-body 1.0.1", "http-body-util", - "hyper", - "hyper-rustls", + "hyper 1.9.0", + "hyper-rustls 0.27.7", "hyper-util", "js-sys", "log", "percent-encoding", "pin-project-lite", - "rustls", + "rustls 0.23.37", "rustls-native-certs", "rustls-pki-types", "serde", @@ -4385,7 +4887,7 @@ dependencies = [ "serde_urlencoded", "sync_wrapper", "tokio", - "tokio-rustls", + "tokio-rustls 0.26.4", "tokio-util", "tower 0.5.3", "tower-http", @@ -4407,25 +4909,25 @@ dependencies = [ "bytes", "futures-core", "futures-util", - "h2", - "http", - "http-body", + "h2 0.4.13", + "http 1.4.0", + "http-body 1.0.1", "http-body-util", - "hyper", - "hyper-rustls", + "hyper 1.9.0", + "hyper-rustls 0.27.7", "hyper-util", "js-sys", "log", "percent-encoding", "pin-project-lite", - "rustls", + "rustls 0.23.37", "rustls-pki-types", "rustls-platform-verifier", "serde", "serde_json", "sync_wrapper", "tokio", - "tokio-rustls", + "tokio-rustls 0.26.4", "tokio-util", "tower 0.5.3", "tower-http", @@ -4778,6 +5280,18 @@ dependencies = [ "windows-sys 0.61.2", ] +[[package]] +name = "rustls" +version = "0.21.12" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3f56a14d1f48b391359b22f731fd4bd7e43c97f3c50eee276f3aa09c94784d3e" +dependencies = [ + "log", + "ring", + "rustls-webpki 0.101.7", + "sct", +] + [[package]] name = "rustls" version = "0.23.37" @@ -4789,7 +5303,7 @@ dependencies = [ "once_cell", "ring", "rustls-pki-types", - "rustls-webpki", + "rustls-webpki 0.103.10", "subtle", "zeroize", ] @@ -4835,10 +5349,10 @@ dependencies = [ "jni", "log", "once_cell", - "rustls", + "rustls 0.23.37", "rustls-native-certs", "rustls-platform-verifier-android", - "rustls-webpki", + "rustls-webpki 0.103.10", "security-framework", "security-framework-sys", "webpki-root-certs", @@ -4851,6 +5365,16 @@ version = "0.1.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "f87165f0995f63a9fbeea62b64d10b4d9d8e78ec6d7d51fb2125fda7bb36788f" +[[package]] +name = "rustls-webpki" +version = "0.101.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8b6275d1ee7a1cd780b64aca7726599a1dbc893b1e64144529e55c3c2f745765" +dependencies = [ + "ring", + "untrusted 0.9.0", +] + [[package]] name = "rustls-webpki" version = "0.103.10" @@ -4964,6 +5488,16 @@ dependencies = [ "sha2 0.11.0", ] +[[package]] +name = "sct" +version = "0.7.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "da046153aa2352493d6cb7da4b6e5c0c057d8a1d0a9aa8560baffdd945acd414" +dependencies = [ + "ring", + "untrusted 0.9.0", +] + [[package]] name = "sd-notify" version = "0.4.5" @@ -5593,7 +6127,7 @@ dependencies = [ "memchr", "once_cell", "percent-encoding", - "rustls", + "rustls 0.23.37", "serde", "serde_json", "sha2 0.10.9", @@ -6061,13 +6595,23 @@ dependencies = [ "syn 2.0.117", ] +[[package]] +name = "tokio-rustls" +version = "0.24.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c28327cf380ac148141087fbfb9de9d7bd4e84ab5d2c28fbc911d753de8a7081" +dependencies = [ + "rustls 0.21.12", + "tokio", +] + [[package]] name = "tokio-rustls" version = "0.26.4" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "1729aa945f29d91ba541258c8df89027d5792d85a8841fb65e8bf0f4ede4ef61" dependencies = [ - "rustls", + "rustls 0.23.37", "tokio", ] @@ -6090,11 +6634,11 @@ checksum = "489a59b6730eda1b0171fcfda8b121f4bee2b35cba8645ca35c5f7ba3eb736c1" dependencies = [ "futures-util", "log", - "rustls", + "rustls 0.23.37", "rustls-native-certs", "rustls-pki-types", "tokio", - "tokio-rustls", + "tokio-rustls 0.26.4", "tungstenite 0.27.0", ] @@ -6153,11 +6697,11 @@ dependencies = [ "axum", "base64 0.22.1", "bytes", - "h2", - "http", - "http-body", + "h2 0.4.13", + "http 1.4.0", + "http-body 1.0.1", "http-body-util", - "hyper", + "hyper 1.9.0", "hyper-timeout", "hyper-util", "percent-encoding", @@ -6233,8 +6777,8 @@ dependencies = [ "bytes", "futures-core", "futures-util", - "http", - "http-body", + "http 1.4.0", + "http-body 1.0.1", "http-body-util", "iri-string", "pin-project-lite", @@ -6332,11 +6876,11 @@ checksum = "eadc29d668c91fcc564941132e17b28a7ceb2f3ebf0b9dae3e03fd7a6748eb0d" dependencies = [ "bytes", "data-encoding", - "http", + "http 1.4.0", "httparse", "log", "rand 0.9.2", - "rustls", + "rustls 0.23.37", "rustls-pki-types", "sha1 0.10.6", "thiserror 2.0.18", @@ -6351,7 +6895,7 @@ checksum = "8628dcc84e5a09eb3d8423d6cb682965dea9133204e8fb3efee74c2a0c259442" dependencies = [ "bytes", "data-encoding", - "http", + "http 1.4.0", "httparse", "log", "rand 0.9.2", @@ -6531,6 +7075,12 @@ version = "0.9.5" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "0b928f33d975fc6ad9f86c8f283853ad26bdd5b10b7f1542aa2fa15e2289105a" +[[package]] +name = "vsimd" +version = "0.8.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5c3082ca00d5a5ef149bb8b555a72ae84c9c59f7250f013ac822ac2e49b19c64" + [[package]] name = "walkdir" version = "2.5.0" @@ -6568,7 +7118,7 @@ dependencies = [ "notify", "rcgen", "reqwest 0.13.2", - "rustls", + "rustls 0.23.37", "schemars 0.9.0", "sd-notify", "sea-orm", @@ -6629,6 +7179,29 @@ dependencies = [ "warpgate-tls", ] +[[package]] +name = "warpgate-aws" +version = "0.22.0-beta.5" +dependencies = [ + "aws-config", + "aws-credential-types", + "aws-sdk-ec2", + "aws-sdk-ec2instanceconnect", + "aws-sdk-eks", + "aws-sdk-rds", + "aws-sdk-sts", + "aws-sigv4", + "aws-smithy-runtime-api", + "dashmap", + "data-encoding", + "http 1.4.0", + "reqwest 0.13.2", + "thiserror 2.0.18", + "tokio", + "tracing", + "url", +] + [[package]] name = "warpgate-ca" version = "0.22.0-beta.5" @@ -6675,7 +7248,7 @@ dependencies = [ "reqwest 0.13.2", "reqwest-websocket", "russh", - "rustls", + "rustls 0.23.37", "rustls-native-certs", "rustls-pki-types", "schemars 0.9.0", @@ -6685,7 +7258,7 @@ dependencies = [ "thiserror 2.0.18", "time", "tokio", - "tokio-rustls", + "tokio-rustls 0.26.4", "tokio-stream", "tokio-tungstenite", "totp-rs", @@ -6693,6 +7266,7 @@ dependencies = [ "tracing-core", "url", "uuid", + "warpgate-aws", "warpgate-ca", "warpgate-ldap", "warpgate-sso", @@ -6738,7 +7312,7 @@ dependencies = [ "rand_chacha 0.10.0", "rand_core 0.10.0", "russh", - "rustls", + "rustls 0.23.37", "sea-orm", "serde", "serde_json", @@ -6834,7 +7408,7 @@ dependencies = [ "data-encoding", "delegate", "futures", - "http", + "http 1.4.0", "percent-encoding", "poem", "poem-openapi", @@ -6851,6 +7425,7 @@ dependencies = [ "url", "uuid", "warpgate-admin", + "warpgate-aws", "warpgate-ca", "warpgate-common", "warpgate-common-http", @@ -6871,14 +7446,14 @@ dependencies = [ "bytes", "dashmap", "futures", - "http", + "http 1.4.0", "md5", "poem", "poem-openapi", "regex", "reqwest 0.13.2", "reqwest-websocket", - "rustls", + "rustls 0.23.37", "sea-orm", "secrecy", "serde", @@ -6886,11 +7461,12 @@ dependencies = [ "thiserror 2.0.18", "time", "tokio", - "tokio-rustls", + "tokio-rustls 0.26.4", "tokio-tungstenite", "tracing", "url", "uuid", + "warpgate-aws", "warpgate-ca", "warpgate-common", "warpgate-common-http", @@ -6911,13 +7487,14 @@ dependencies = [ "mysql_common", "password-hash", "rand 0.10.0", - "rustls", + "rustls 0.23.37", "sha1 0.10.6", "thiserror 2.0.18", "tokio", - "tokio-rustls", + "tokio-rustls 0.26.4", "tracing", "uuid", + "warpgate-aws", "warpgate-common", "warpgate-core", "warpgate-database-protocols", @@ -6937,14 +7514,15 @@ dependencies = [ "humantime", "pgwire", "rsasl", - "rustls", + "rustls 0.23.37", "rustls-native-certs", "socket2 0.5.10", "thiserror 2.0.18", "tokio", - "tokio-rustls", + "tokio-rustls 0.26.4", "tracing", "uuid", + "warpgate-aws", "warpgate-common", "warpgate-core", "warpgate-tls", @@ -6971,6 +7549,7 @@ dependencies = [ "tokio", "tracing", "uuid", + "warpgate-aws", "warpgate-common", "warpgate-core", "warpgate-db-entities", @@ -7002,7 +7581,7 @@ version = "0.22.0-beta.5" dependencies = [ "poem", "poem-openapi", - "rustls", + "rustls 0.23.37", "rustls-native-certs", "rustls-pki-types", "sea-orm", @@ -7010,7 +7589,7 @@ dependencies = [ "serde_json", "thiserror 2.0.18", "tokio", - "tokio-rustls", + "tokio-rustls 0.26.4", "tracing", "webpki", "x509-parser 0.17.0", @@ -7861,6 +8440,12 @@ dependencies = [ "time", ] +[[package]] +name = "xmlparser" +version = "0.13.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "66fee0b777b0f5ac1c69bb06d361268faafa61cd4682ae064a171c16c433e9e4" + [[package]] name = "yaml-rust2" version = "0.10.4" @@ -7918,14 +8503,14 @@ checksum = "ef19a12dfb29fe39f78e1547e1be49717b84aef8762a4001359ed4f94d3accc1" dependencies = [ "async-trait", "base64 0.22.1", - "http", + "http 1.4.0", "http-body-util", - "hyper", - "hyper-rustls", + "hyper 1.9.0", + "hyper-rustls 0.27.7", "hyper-util", "log", "percent-encoding", - "rustls", + "rustls 0.23.37", "seahash", "serde", "serde_json", diff --git a/Cargo.toml b/Cargo.toml index 77d6aec6b..c1e34b937 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -1,9 +1,10 @@ -# cargo-features = ["profile-rustflags"] +cargo-features = ["profile-rustflags"] [workspace] members = [ "warpgate", "warpgate-admin", + "warpgate-aws", "warpgate-common", "warpgate-common-http", "warpgate-tls", @@ -98,3 +99,21 @@ strip = "debuginfo" [profile.coverage] inherits = "dev" # rustflags = ["-Cinstrument-coverage"] + +[profile.dev.package.aws-sdk-ec2] +rustflags = ["-Zhint-mostly-unused"] + +[profile.release.package.aws-sdk-ec2] +rustflags = ["-Zhint-mostly-unused"] + +[profile.dev.package.aws-sdk-rds] +rustflags = ["-Zhint-mostly-unused"] + +[profile.release.package.aws-sdk-rds] +rustflags = ["-Zhint-mostly-unused"] + +[profile.dev.package.aws-sdk-eks] +rustflags = ["-Zhint-mostly-unused"] + +[profile.release.package.aws-sdk-eks] +rustflags = ["-Zhint-mostly-unused"] diff --git a/tests/test_postgres_user_auth_in_browser.py b/tests/test_postgres_user_auth_in_browser.py index fc2950839..9b165703f 100644 --- a/tests/test_postgres_user_auth_in_browser.py +++ b/tests/test_postgres_user_auth_in_browser.py @@ -47,7 +47,12 @@ async def test( host="localhost", port=db_port, username="user", - password="123", + auth=sdk.DatabaseTargetAuth( + sdk.DatabaseTargetAuthDatabaseTargetPasswordAuth( + kind="Password", + password="123", + ) + ), tls=sdk.Tls( mode=sdk.TlsMode.PREFERRED, verify=False, @@ -73,7 +78,11 @@ async def test( headers=headers, ssl=False, ) - ws = await session.ws_connect(url.replace('https:', 'wss:') + '/@warpgate/api/auth/web-auth-requests/stream', ssl=False) + ws = await session.ws_connect( + url.replace("https:", "wss:") + + "/@warpgate/api/auth/web-auth-requests/stream", + ssl=False, + ) client = processes.start( [ @@ -97,10 +106,14 @@ async def test( msg = await ws.receive(5) auth_id = msg.data - auth_state = await (await session.get(f'{url}/@warpgate/api/auth/state/{auth_id}', ssl=False)).json() - assert auth_state['protocol'] == 'PostgreSQL' - assert auth_state['state'] == 'WebUserApprovalNeeded' - r = await session.post(f'{url}/@warpgate/api/auth/state/{auth_id}/approve', ssl=False) + auth_state = await ( + await session.get(f"{url}/@warpgate/api/auth/state/{auth_id}", ssl=False) + ).json() + assert auth_state["protocol"] == "PostgreSQL" + assert auth_state["state"] == "WebUserApprovalNeeded" + r = await session.post( + f"{url}/@warpgate/api/auth/state/{auth_id}/approve", ssl=False + ) assert r.status == 200 client.stdin.write(b"\r\n") diff --git a/tests/test_postgres_user_auth_password.py b/tests/test_postgres_user_auth_password.py index 497e4a298..63f645540 100644 --- a/tests/test_postgres_user_auth_password.py +++ b/tests/test_postgres_user_auth_password.py @@ -23,20 +23,29 @@ def test( user.id, sdk.NewPasswordCredential(password="123") ) api.add_user_role(user.id, role.id) - target = api.create_target(sdk.TargetDataRequest( - name=f"postgres-{uuid4()}", - options=sdk.TargetOptions(sdk.TargetOptionsTargetPostgresOptions( - kind="Postgres", - host="localhost", - port=db_port, - username="user", - password="123", - tls=sdk.Tls( - mode=sdk.TlsMode.PREFERRED, - verify=False, + target = api.create_target( + sdk.TargetDataRequest( + name=f"postgres-{uuid4()}", + options=sdk.TargetOptions( + sdk.TargetOptionsTargetPostgresOptions( + kind="Postgres", + host="localhost", + port=db_port, + username="user", + auth=sdk.DatabaseTargetAuth( + sdk.DatabaseTargetAuthDatabaseTargetPasswordAuth( + kind="Password", + password="123", + ) + ), + tls=sdk.Tls( + mode=sdk.TlsMode.PREFERRED, + verify=False, + ), + ) ), - )), - )) + ) + ) api.add_target_role(target.id, role.id) wait_port(db_port, recv=False) diff --git a/warpgate-admin/src/api/sessions_list.rs b/warpgate-admin/src/api/sessions_list.rs index ace97a545..db76cd5d2 100644 --- a/warpgate-admin/src/api/sessions_list.rs +++ b/warpgate-admin/src/api/sessions_list.rs @@ -33,6 +33,7 @@ enum CloseAllSessionsResponse { impl Api { #[allow(clippy::too_many_arguments)] #[oai(path = "/sessions", method = "get", operation_id = "get_sessions")] + #[allow(clippy::too_many_arguments)] async fn api_get_all_sessions( &self, ctx: Data<&AuthenticatedRequestContext>, diff --git a/warpgate-admin/src/api/targets.rs b/warpgate-admin/src/api/targets.rs index aaaa9737b..3d7fee991 100644 --- a/warpgate-admin/src/api/targets.rs +++ b/warpgate-admin/src/api/targets.rs @@ -124,12 +124,24 @@ impl ListApi { ))); } + let mut options = body.options.clone(); + + match &mut options { + TargetOptions::MySql(opts) => { + opts.normalize(); + } + TargetOptions::Postgres(opts) => { + opts.normalize(); + } + _ => {} + } + let values = Target::ActiveModel { id: Set(Uuid::new_v4()), name: Set(body.name.clone()), description: Set(body.description.clone().unwrap_or_default()), - kind: Set((&body.options).into()), - options: Set(serde_json::to_value(body.options.clone()).map_err(WarpgateError::from)?), + kind: Set((&options).into()), + options: Set(serde_json::to_value(options.clone()).map_err(WarpgateError::from)?), rate_limit_bytes_per_second: Set(None), group_id: Set(body.group_id), }; diff --git a/warpgate-aws/Cargo.toml b/warpgate-aws/Cargo.toml new file mode 100644 index 000000000..430b02613 --- /dev/null +++ b/warpgate-aws/Cargo.toml @@ -0,0 +1,41 @@ +[package] +edition = "2021" +license = "Apache-2.0" +name = "warpgate-aws" +version = "0.22.0-beta.5" +publish = false + +[dependencies] +aws-config = { version = "1", default-features = false, features = [ + "behavior-version-latest", + "rustls", + "rt-tokio", +] } +aws-sdk-ec2 = { version = "1", default-features = false, features = [ + "behavior-version-latest", + "rustls", +] } +aws-sdk-ec2instanceconnect = { version = "1", default-features = false, features = [ + "behavior-version-latest", + "rustls", +] } +aws-sdk-eks = { version = "1", default-features = false, features = [ + "behavior-version-latest", + "rustls", +] } +aws-sdk-sts = { version = "1", default-features = false, features = [ + "behavior-version-latest", + "rustls", +] } +aws-sigv4 = { version = "1", default-features = false } +aws-credential-types = { version = "1", default-features = false } +aws-smithy-runtime-api = { version = "1", default-features = false } +dashmap = { version = "6.0", default-features = false } +http = "1" +reqwest.workspace = true +tokio.workspace = true +tracing.workspace = true +url = { version = "2.4", default-features = false } +data-encoding.workspace = true +aws-sdk-rds = "1.130.0" +thiserror.workspace = true diff --git a/warpgate-aws/src/ec2.rs b/warpgate-aws/src/ec2.rs new file mode 100644 index 000000000..cc49f6f3c --- /dev/null +++ b/warpgate-aws/src/ec2.rs @@ -0,0 +1,170 @@ +use std::time::Duration; + +use tracing::{debug, info}; + +use crate::{instance_cache, AwsError}; + +#[derive(Debug, Clone)] +pub struct Ec2InstanceInfo { + pub instance_id: String, + pub availability_zone: String, + pub region: String, +} + +/// Detect if running on EC2 by querying the IMDS endpoint with a 1s timeout. +pub async fn is_running_on_ec2() -> bool { + let Ok(client) = reqwest::Client::builder() + .timeout(Duration::from_secs(1)) + .build() + else { + return false; + }; + + // Try IMDSv2 token endpoint + let result = tokio::time::timeout( + Duration::from_secs(2), + client + .put("http://169.254.169.254/latest/api/token") + .header("X-aws-ec2-metadata-token-ttl-seconds", "21600") + .send(), + ) + .await; + + match result { + Ok(Ok(resp)) if resp.status().is_success() => { + info!("Detected EC2 environment via IMDS"); + true + } + _ => { + debug!("Not running on EC2 (IMDS not reachable)"); + false + } + } +} + +/// Look up an EC2 instance by IP address across all regions (cached) +pub async fn find_instance_by_ip(ip: &str) -> Result { + if let Some(entry) = instance_cache().get(ip) { + return Ok(entry.value().clone()); + } + + let regions = list_all_regions().await?; + let ip = ip.to_string(); + + // Query all regions in parallel + let mut handles = Vec::new(); + for region_name in regions { + let ip = ip.clone(); + handles.push(tokio::spawn(async move { + find_instance_in_region(&ip, ®ion_name).await + })); + } + + for handle in handles { + if let Ok(Ok(Some(info))) = handle.await { + instance_cache().insert(ip.clone(), info.clone()); + return Ok(info); + } + } + + Err(AwsError::RegionUnknown(ip)) +} + +async fn find_instance_in_region( + ip: &str, + region_name: &str, +) -> Result, AwsError> { + let region = aws_sdk_ec2::config::Region::new(region_name.to_string()); + let config = aws_config::defaults(aws_config::BehaviorVersion::latest()) + .region(region) + .load() + .await; + let client = aws_sdk_ec2::Client::new(&config); + + // Try private IP first, then public IP + for filter_name in ["private-ip-address", "ip-address"] { + let result = client + .describe_instances() + .filters( + aws_sdk_ec2::types::Filter::builder() + .name(filter_name) + .values(ip) + .build(), + ) + .send() + .await; + + if let Ok(output) = result { + for reservation in output.reservations() { + for instance in reservation.instances() { + if let (Some(instance_id), Some(az)) = ( + instance.instance_id(), + instance.placement().and_then(|p| p.availability_zone()), + ) { + let info = Ec2InstanceInfo { + instance_id: instance_id.to_string(), + availability_zone: az.to_string(), + region: region_name.to_string(), + }; + debug!(?info, "Found EC2 instance for IP {ip}"); + return Ok(Some(info)); + } + } + } + } + } + + Ok(None) +} + +async fn list_all_regions() -> Result, AwsError> { + let config = aws_config::defaults(aws_config::BehaviorVersion::latest()) + .load() + .await; + let client = aws_sdk_ec2::Client::new(&config); + + let output = client + .describe_regions() + .all_regions(true) + .send() + .await + .map_err(AwsError::sdk_error)?; + + Ok(output + .regions() + .iter() + .filter_map(|r| r.region_name().map(|s| s.to_string())) + .collect()) +} + +/// Push an SSH public key to an EC2 instance via EC2 Instance Connect. +pub async fn send_ssh_public_key( + instance_id: &str, + availability_zone: &str, + region: &str, + os_user: &str, + ssh_public_key: &str, +) -> Result<(), AwsError> { + let region_obj = aws_sdk_ec2instanceconnect::config::Region::new(region.to_string()); + let config = aws_config::defaults(aws_config::BehaviorVersion::latest()) + .region(region_obj) + .load() + .await; + + let client = aws_sdk_ec2instanceconnect::Client::new(&config); + client + .send_ssh_public_key() + .instance_id(instance_id) + .instance_os_user(os_user) + .ssh_public_key(ssh_public_key) + .availability_zone(availability_zone) + .send() + .await + .map_err(AwsError::sdk_error)?; + + info!( + instance_id, + os_user, "Pushed SSH public key via EC2 Instance Connect" + ); + Ok(()) +} diff --git a/warpgate-aws/src/eks.rs b/warpgate-aws/src/eks.rs new file mode 100644 index 000000000..db34fc71b --- /dev/null +++ b/warpgate-aws/src/eks.rs @@ -0,0 +1,128 @@ +use tracing::{debug, info}; + +use crate::error::AwsResourceType; +use crate::region::parse_eks_region; +use crate::AwsError; + +pub struct EksClusterInfo { + pub name: String, + pub region: String, +} + +/// Find the EKS cluster name that matches the given API server URL. +pub async fn find_eks_cluster_by_url(cluster_url: &str) -> Result { + let region_name = + parse_eks_region(cluster_url).ok_or_else(|| AwsError::RegionUnknown(cluster_url.into()))?; + + let region = aws_sdk_eks::config::Region::new(region_name.clone()); + let config = aws_config::defaults(aws_config::BehaviorVersion::latest()) + .region(region) + .load() + .await; + let client = aws_sdk_eks::Client::new(&config); + + let clusters = client + .list_clusters() + .send() + .await + .map_err(AwsError::sdk_error)?; + + let normalized_url = cluster_url.trim_end_matches('/'); + + for cluster_name in clusters.clusters() { + let describe = client.describe_cluster().name(cluster_name).send().await; + + if let Ok(output) = describe { + if let Some(cluster) = output.cluster() { + if let Some(endpoint) = cluster.endpoint() { + if endpoint.trim_end_matches('/') == normalized_url { + info!(cluster_name, "Matched EKS cluster by endpoint URL"); + return Ok(EksClusterInfo { + name: cluster_name.clone(), + region: region_name, + }); + } + } + } + } + } + + Err(AwsError::ResourceNotFound( + AwsResourceType::EksCluster, + cluster_url.into(), + )) +} + +/// Generate an EKS authentication token using a presigned STS GetCallerIdentity request. +/// +/// This produces a token in the format `k8s-aws-v1.`, +/// compatible with the `aws-iam-authenticator` / EKS token exchange. +pub async fn generate_eks_token(cluster_name: &str, region: &str) -> Result { + // EKS rust SDK doesn't have a convenience fn for this like the RDS SDK + use std::time::SystemTime; + + use aws_credential_types::provider::ProvideCredentials; + use aws_sigv4::http_request::{ + sign, SignableBody, SignableRequest, SignatureLocation, SigningSettings, + }; + use aws_sigv4::sign::v4; + + let region_obj = aws_sdk_sts::config::Region::new(region.to_string()); + let config = aws_config::defaults(aws_config::BehaviorVersion::latest()) + .region(region_obj) + .load() + .await; + + let credentials = config + .credentials_provider() + .ok_or(AwsError::NoCredentials)? + .provide_credentials() + .await?; + + let identity = credentials.into(); + + // Build the presigned URL for STS GetCallerIdentity + let mut signing_settings = SigningSettings::default(); + signing_settings.signature_location = SignatureLocation::QueryParams; + signing_settings.expires_in = Some(std::time::Duration::from_secs(60)); + + let signing_params = v4::SigningParams::builder() + .identity(&identity) + .region(region) + .name("sts") + .time(SystemTime::now()) + .settings(signing_settings) + .build()?; + + // The URL we want to presign + let url = + format!("https://sts.{region}.amazonaws.com/?Action=GetCallerIdentity&Version=2011-06-15"); + + let signable_request = SignableRequest::new( + "GET", + &url, + [("x-k8s-aws-id", cluster_name)].into_iter(), + SignableBody::Bytes(&[]), + )?; + + let (signing_instructions, _signature) = + sign(signable_request, &signing_params.into())?.into_parts(); + + // Build an http::Request and apply signing instructions to get the presigned URL + let mut request = http::Request::builder() + .method("GET") + .uri(&url) + .header("x-k8s-aws-id", cluster_name) + .body(())?; + signing_instructions.apply_to_request_http1x(&mut request); + + // Base64url-encode the full presigned URI (no padding) + let signed_url = request.uri().to_string(); + let token = format!( + "k8s-aws-v1.{}", + data_encoding::BASE64URL_NOPAD.encode(signed_url.as_bytes()) + ); + + debug!(cluster_name, "Generated EKS authentication token"); + Ok(token) +} diff --git a/warpgate-aws/src/error.rs b/warpgate-aws/src/error.rs new file mode 100644 index 000000000..fdac26d2a --- /dev/null +++ b/warpgate-aws/src/error.rs @@ -0,0 +1,38 @@ +use std::error::Error; + +use aws_credential_types::provider::error::CredentialsError; +use aws_sigv4::http_request::SigningError; +use aws_sigv4::sign::v4::signing_params::BuildError; + +#[derive(Debug)] +pub enum AwsResourceType { + Ec2Instance, + EksCluster, + RdsInstance, +} + +#[derive(thiserror::Error, Debug)] +pub enum AwsError { + #[error("cannot determine region for {0}")] + RegionUnknown(String), + #[error("{0:?} resource not found: {1}")] + ResourceNotFound(AwsResourceType, String), + #[error("no AWS credentials available")] + NoCredentials, + #[error("credentials: {0}")] + Credentials(#[from] CredentialsError), + #[error("signing parameters: {0}")] + SigningParams(#[from] BuildError), + #[error("signing: {0}")] + Signing(#[from] SigningError), + #[error("HTTP: {0}")] + Http(#[from] http::Error), + #[error(transparent)] + Other(#[from] Box), +} + +impl AwsError { + pub fn sdk_error(err: E) -> Self { + Self::Other(Box::new(err)) + } +} diff --git a/warpgate-aws/src/lib.rs b/warpgate-aws/src/lib.rs new file mode 100644 index 000000000..be0716565 --- /dev/null +++ b/warpgate-aws/src/lib.rs @@ -0,0 +1,44 @@ +use std::sync::OnceLock; + +use dashmap::DashMap; +use tokio::sync::OnceCell; + +mod ec2; +mod eks; +mod error; +mod rds; +mod region; + +pub use ec2::{find_instance_by_ip, is_running_on_ec2, send_ssh_public_key, Ec2InstanceInfo}; +pub use eks::{find_eks_cluster_by_url, generate_eks_token, EksClusterInfo}; +pub use error::AwsError; +pub use rds::generate_rds_auth_token; +pub use region::{get_imds_region, parse_eks_region, parse_rds_region}; + +/// Cached EC2 detection result +static EC2_DETECTION: OnceCell = OnceCell::const_new(); + +/// Cached IMDS region +static IMDS_REGION: OnceCell> = OnceCell::const_new(); + +/// Cached IP -> Ec2InstanceInfo +static INSTANCE_CACHE: OnceLock> = OnceLock::new(); + +fn instance_cache() -> &'static DashMap { + INSTANCE_CACHE.get_or_init(DashMap::new) +} + +/// Check if running on EC2 (cached, 1s timeout on first call) +pub async fn check_ec2() -> bool { + *EC2_DETECTION + .get_or_init(|| async { is_running_on_ec2().await }) + .await +} + +/// Get the IMDS region (cached) +pub async fn cached_imds_region() -> Option { + IMDS_REGION + .get_or_init(|| async { get_imds_region().await }) + .await + .clone() +} diff --git a/warpgate-aws/src/rds.rs b/warpgate-aws/src/rds.rs new file mode 100644 index 000000000..e56a88bc6 --- /dev/null +++ b/warpgate-aws/src/rds.rs @@ -0,0 +1,37 @@ +use aws_sdk_rds::auth_token::AuthTokenGenerator; + +use crate::region::parse_rds_region; +use crate::AwsError; + +/// Generate an RDS IAM authentication token. +/// +/// This token is a presigned URL that can be used as a password to connect +/// to an RDS instance using IAM authentication. +pub async fn generate_rds_auth_token( + host: &str, + port: u16, + username: &str, +) -> Result { + let region_name = parse_rds_region(host).ok_or_else(|| AwsError::RegionUnknown(host.into()))?; + + let region = aws_sdk_sts::config::Region::new(region_name.clone()); + let config = aws_config::defaults(aws_config::BehaviorVersion::latest()) + .region(region) + .load() + .await; + + let generator = AuthTokenGenerator::new( + aws_sdk_rds::auth_token::Config::builder() + .hostname(host) + .port(u64::from(port)) + .username(username) + .build() + .map_err(AwsError::from)?, + ); + let token = generator + .auth_token(&config) + .await + .map_err(AwsError::from)?; + + Ok(token.to_string()) +} diff --git a/warpgate-aws/src/region.rs b/warpgate-aws/src/region.rs new file mode 100644 index 000000000..872114848 --- /dev/null +++ b/warpgate-aws/src/region.rs @@ -0,0 +1,108 @@ +use std::time::Duration; + +use tracing::debug; + +/// Parse the AWS region from an EKS cluster API URL. +/// +/// EKS URLs follow the pattern: `https://...eks.amazonaws.com` +pub fn parse_eks_region(url: &str) -> Option { + let host = url::Url::parse(url).ok()?.host_str()?.to_string(); + // e.g. ABCD1234.gr7.us-east-1.eks.amazonaws.com + let parts: Vec<&str> = host.split('.').collect(); + // Find "eks" and "amazonaws" in the parts + if let Some(eks_pos) = parts.iter().position(|&p| p == "eks") { + if eks_pos >= 1 { + #[allow(clippy::indexing_slicing)] // known index + let region = parts[eks_pos - 1]; + if region.contains('-') { + return Some(region.to_string()); + } + } + } + None +} + +/// Parse the AWS region from an RDS endpoint hostname. +/// +/// RDS endpoints follow patterns like: +/// - `mydb.abc123.us-east-1.rds.amazonaws.com` +/// - `mydb.cluster-abc123.us-east-1.rds.amazonaws.com` (Aurora) +pub fn parse_rds_region(host: &str) -> Option { + let parts: Vec<&str> = host.split('.').collect(); + // Find "rds" and "amazonaws" in the parts + if let Some(rds_pos) = parts.iter().position(|&p| p == "rds") { + if rds_pos >= 1 { + #[allow(clippy::indexing_slicing)] // known index + let region = parts[rds_pos - 1]; + if region.contains('-') { + return Some(region.to_string()); + } + } + } + None +} + +/// Get the region from EC2 Instance Metadata Service (IMDS). +pub async fn get_imds_region() -> Option { + let client = reqwest::Client::builder() + .timeout(Duration::from_secs(2)) + .build() + .ok()?; + + // Get IMDSv2 token + let token = client + .put("http://169.254.169.254/latest/api/token") + .header("X-aws-ec2-metadata-token-ttl-seconds", "21600") + .send() + .await + .ok()? + .text() + .await + .ok()?; + + // Get region + let region = client + .get("http://169.254.169.254/latest/meta-data/placement/region") + .header("X-aws-ec2-metadata-token", &token) + .send() + .await + .ok()? + .text() + .await + .ok()?; + + debug!(region, "Detected AWS region from IMDS"); + Some(region) +} + +#[cfg(test)] +mod tests { + use super::*; + + #[test] + fn test_parse_eks_region() { + assert_eq!( + parse_eks_region("https://ABCD1234.gr7.us-east-1.eks.amazonaws.com"), + Some("us-east-1".to_string()) + ); + assert_eq!( + parse_eks_region("https://xyz.sk1.eu-west-2.eks.amazonaws.com"), + Some("eu-west-2".to_string()) + ); + assert_eq!(parse_eks_region("https://example.com"), None); + assert_eq!(parse_eks_region("not-a-url"), None); + } + + #[test] + fn test_parse_rds_region() { + assert_eq!( + parse_rds_region("mydb.abc123.us-east-1.rds.amazonaws.com"), + Some("us-east-1".to_string()) + ); + assert_eq!( + parse_rds_region("mydb.cluster-abc123.eu-west-1.rds.amazonaws.com"), + Some("eu-west-1".to_string()) + ); + assert_eq!(parse_rds_region("localhost"), None); + } +} diff --git a/warpgate-common/Cargo.toml b/warpgate-common/Cargo.toml index 3c79900b6..6a9d122c7 100644 --- a/warpgate-common/Cargo.toml +++ b/warpgate-common/Cargo.toml @@ -50,6 +50,7 @@ tracing-core = { version = "0.1", default-features = false } tracing.workspace = true url = { version = "2.2", default-features = false } uuid.workspace = true +warpgate-aws = { path = "../warpgate-aws", default-features = false } warpgate-ca = { path = "../warpgate-ca", default-features = false } warpgate-ldap = { path = "../warpgate-ldap" } warpgate-sso = { path = "../warpgate-sso", default-features = false } diff --git a/warpgate-common/src/config/target.rs b/warpgate-common/src/config/target.rs index 299547866..262fa49ca 100644 --- a/warpgate-common/src/config/target.rs +++ b/warpgate-common/src/config/target.rs @@ -40,13 +40,15 @@ pub struct TargetSSHOptions { } #[derive(Debug, Deserialize, Serialize, Clone, PartialEq, Eq, Union)] -#[serde(untagged)] +#[serde(tag = "kind")] #[oai(discriminator_name = "kind", one_of)] pub enum SSHTargetAuth { #[serde(rename = "password")] Password(SshTargetPasswordAuth), #[serde(rename = "publickey")] PublicKey(SshTargetPublicKeyAuth), + #[serde(rename = "iam_role")] + IamRole(SshTargetIamRoleAuth), } #[derive(Debug, Deserialize, Serialize, Clone, PartialEq, Eq, Object)] @@ -57,6 +59,9 @@ pub struct SshTargetPasswordAuth { #[derive(Debug, Deserialize, Serialize, Clone, PartialEq, Eq, Object, Default)] pub struct SshTargetPublicKeyAuth {} +#[derive(Debug, Deserialize, Serialize, Clone, PartialEq, Eq, Object, Default)] +pub struct SshTargetIamRoleAuth {} + impl Default for SSHTargetAuth { fn default() -> Self { Self::PublicKey(SshTargetPublicKeyAuth::default()) @@ -97,6 +102,40 @@ impl Default for Tls { } } +#[derive(Debug, Deserialize, Serialize, Clone, PartialEq, Eq, Union)] +#[serde(tag = "kind")] +#[oai(discriminator_name = "kind", one_of)] +pub enum DatabaseTargetAuth { + #[serde(rename = "password")] + Password(DatabaseTargetPasswordAuth), + #[serde(rename = "iam_role")] + IamRole(DatabaseTargetIamRoleAuth), +} + +#[derive(Debug, Deserialize, Serialize, Clone, PartialEq, Eq, Object, Default)] +pub struct DatabaseTargetPasswordAuth { + #[serde(default)] + pub password: String, +} + +#[derive(Debug, Deserialize, Serialize, Clone, PartialEq, Eq, Object, Default)] +pub struct DatabaseTargetIamRoleAuth {} + +impl Default for DatabaseTargetAuth { + fn default() -> Self { + Self::Password(DatabaseTargetPasswordAuth::default()) + } +} + +impl DatabaseTargetAuth { + pub fn password(&self) -> Option<&str> { + match self { + Self::Password(auth) => Some(auth.password.as_str()), + Self::IamRole(_) => None, + } + } +} + #[derive(Debug, Deserialize, Serialize, Clone, Object)] pub struct TargetMySqlOptions { #[serde(default = "_default_empty_string")] @@ -109,7 +148,12 @@ pub struct TargetMySqlOptions { pub username: String, #[serde(default)] - pub password: Option, + auth: Option, + + /// Deprecated: use `auth` instead. Kept for backward compatibility with old configs/API clients. + #[serde(default, skip_serializing)] + #[oai(deprecated)] + password: Option, #[serde(default)] pub tls: Tls, @@ -118,6 +162,26 @@ pub struct TargetMySqlOptions { pub default_database_name: Option, } +impl TargetMySqlOptions { + pub fn effective_auth(&self) -> DatabaseTargetAuth { + if let Some(auth) = &self.auth { + auth.clone() + } else { + DatabaseTargetAuth::Password(DatabaseTargetPasswordAuth { + password: self.password.clone().unwrap_or_default(), + }) + } + } + + pub fn normalize(&mut self) { + if let Some(password) = self.password.take() { + self.auth = Some(DatabaseTargetAuth::Password(DatabaseTargetPasswordAuth { + password: password, + })); + } + } +} + #[derive(Debug, Deserialize, Serialize, Clone, Object)] pub struct TargetPostgresOptions { #[serde(default = "_default_empty_string")] @@ -130,7 +194,12 @@ pub struct TargetPostgresOptions { pub username: String, #[serde(default)] - pub password: Option, + auth: Option, + + /// Deprecated: use `auth` instead. Kept for backward compatibility with old configs/API clients. + #[serde(default, skip_serializing)] + #[oai(deprecated)] + password: Option, #[serde(default)] pub tls: Tls, @@ -142,6 +211,26 @@ pub struct TargetPostgresOptions { pub default_database_name: Option, } +impl TargetPostgresOptions { + pub fn effective_auth(&self) -> DatabaseTargetAuth { + if let Some(auth) = &self.auth { + auth.clone() + } else { + DatabaseTargetAuth::Password(DatabaseTargetPasswordAuth { + password: self.password.clone().unwrap_or_default(), + }) + } + } + + pub fn normalize(&mut self) { + if let Some(password) = self.password.take() { + self.auth = Some(DatabaseTargetAuth::Password(DatabaseTargetPasswordAuth { + password: password, + })); + } + } +} + #[derive(Debug, Deserialize, Serialize, Clone, Object)] pub struct TargetKubernetesOptions { #[serde(default = "_default_empty_string")] @@ -155,13 +244,15 @@ pub struct TargetKubernetesOptions { } #[derive(Debug, Deserialize, Serialize, Clone, PartialEq, Eq, Union)] -#[serde(untagged)] +#[serde(tag = "kind")] #[oai(discriminator_name = "kind", one_of)] pub enum KubernetesTargetAuth { #[serde(rename = "token")] Token(KubernetesTargetTokenAuth), #[serde(rename = "certificate")] Certificate(KubernetesTargetCertificateAuth), + #[serde(rename = "iam_role")] + IamRole(KubernetesTargetIamRoleAuth), } #[derive(Debug, Deserialize, Serialize, Clone, PartialEq, Eq, Object)] @@ -169,6 +260,9 @@ pub struct KubernetesTargetTokenAuth { pub token: Secret, } +#[derive(Debug, Deserialize, Serialize, Clone, PartialEq, Eq, Object, Default)] +pub struct KubernetesTargetIamRoleAuth {} + impl Default for KubernetesTargetAuth { fn default() -> Self { Self::Certificate(KubernetesTargetCertificateAuth::default()) diff --git a/warpgate-common/src/error.rs b/warpgate-common/src/error.rs index b646a8359..3cde12d73 100644 --- a/warpgate-common/src/error.rs +++ b/warpgate-common/src/error.rs @@ -3,6 +3,7 @@ use std::error::Error; use poem::error::ResponseError; use poem_openapi::ApiResponse; use uuid::Uuid; +use warpgate_aws::AwsError; use warpgate_ca::CaError; use warpgate_sso::SsoError; use warpgate_tls::RustlsSetupError; @@ -63,6 +64,8 @@ pub enum WarpgateError { NoAdminAccess, #[error("admin permission required: {0:?}")] NoAdminPermission(AdminPermission), + #[error("AWS: {0}")] + Aws(AwsError), } impl ResponseError for WarpgateError { @@ -77,6 +80,12 @@ impl ResponseError for WarpgateError { } } +impl From> for WarpgateError { + fn from(err: Box) -> Self { + Self::Other(err) + } +} + impl WarpgateError { pub fn other(err: E) -> Self { Self::Other(Box::new(err)) diff --git a/warpgate-db-entities/src/Target.rs b/warpgate-db-entities/src/Target.rs index 1fcea5c11..b00b7a6b4 100644 --- a/warpgate-db-entities/src/Target.rs +++ b/warpgate-db-entities/src/Target.rs @@ -31,15 +31,6 @@ impl From<&TargetOptions> for TargetKind { } } -#[derive(Debug, PartialEq, Eq, Serialize, Clone, Enum, EnumIter, DeriveActiveEnum)] -#[sea_orm(rs_type = "String", db_type = "String(StringLen::N(16))")] -pub enum SshAuthKind { - #[sea_orm(string_value = "password")] - Password, - #[sea_orm(string_value = "publickey")] - PublicKey, -} - #[derive(Clone, Debug, PartialEq, Eq, DeriveEntityModel, Serialize, Object)] #[sea_orm(table_name = "targets")] #[oai(rename = "Target")] diff --git a/warpgate-db-migrations/src/lib.rs b/warpgate-db-migrations/src/lib.rs index 07cf924df..fecaaaf1c 100644 --- a/warpgate-db-migrations/src/lib.rs +++ b/warpgate-db-migrations/src/lib.rs @@ -38,6 +38,8 @@ mod m00033_add_log_target; mod m00034_add_log_related_fields; mod m00035_ticket_user_target_id; mod m00036_user_role_expiry_history; +mod m00037_database_target_auth; +mod m00038_fix_target_auth_tags; pub struct Migrator; @@ -81,6 +83,8 @@ impl MigratorTrait for Migrator { Box::new(m00034_add_log_related_fields::Migration), Box::new(m00035_ticket_user_target_id::Migration), Box::new(m00036_user_role_expiry_history::Migration), + Box::new(m00037_database_target_auth::Migration), + Box::new(m00038_fix_target_auth_tags::Migration), ] } } diff --git a/warpgate-db-migrations/src/m00037_database_target_auth.rs b/warpgate-db-migrations/src/m00037_database_target_auth.rs new file mode 100644 index 000000000..74ffc5ad7 --- /dev/null +++ b/warpgate-db-migrations/src/m00037_database_target_auth.rs @@ -0,0 +1,115 @@ +use sea_orm::{ActiveModelTrait, EntityTrait, Set}; +use sea_orm_migration::prelude::*; +use tracing::error; + +mod target { + use sea_orm::entity::prelude::*; + + #[derive(Debug, Clone, PartialEq, Eq, EnumIter, DeriveActiveEnum)] + #[sea_orm(rs_type = "String", db_type = "String(StringLen::None)")] + pub enum TargetKind { + #[sea_orm(string_value = "http")] + Http, + #[sea_orm(string_value = "kubernetes")] + Kubernetes, + #[sea_orm(string_value = "mysql")] + MySql, + #[sea_orm(string_value = "postgres")] + Postgres, + #[sea_orm(string_value = "ssh")] + Ssh, + #[sea_orm(string_value = "web_admin")] + WebAdmin, + } + + #[derive(Clone, Debug, PartialEq, Eq, DeriveEntityModel)] + #[sea_orm(table_name = "targets")] + pub struct Model { + #[sea_orm(primary_key, auto_increment = false)] + pub id: Uuid, + pub name: String, + pub kind: TargetKind, + pub options: serde_json::Value, + } + + #[derive(Copy, Clone, Debug, EnumIter, DeriveRelation)] + pub enum Relation {} + + impl ActiveModelBehavior for ActiveModel {} +} + +pub struct Migration; + +impl MigrationName for Migration { + fn name(&self) -> &str { + "m00037_database_target_auth" + } +} + +#[async_trait::async_trait] +impl MigrationTrait for Migration { + async fn up(&self, manager: &SchemaManager) -> Result<(), DbErr> { + let db = manager.get_connection(); + + let targets = target::Entity::find().all(db).await?; + + for t in targets { + let is_db_target = matches!( + t.kind, + target::TargetKind::MySql | target::TargetKind::Postgres + ); + if !is_db_target { + continue; + } + + let Some(options_obj) = t.options.as_object() else { + error!(target_id = %t.id, "Target options is not a JSON object, skipping"); + continue; + }; + + // Options are wrapped under a protocol key, e.g. {"mysql": {"password": "...", ...}} + let kind_key = match t.kind { + target::TargetKind::MySql => "mysql", + target::TargetKind::Postgres => "postgres", + _ => continue, + }; + + let Some(proto_obj) = options_obj.get(kind_key).and_then(|v| v.as_object()) else { + error!(target_id = %t.id, "Target options missing protocol key, skipping"); + continue; + }; + + let mut new_proto = proto_obj.clone(); + + // Extract the old password field + let password = new_proto.remove("password"); + + // Build the new auth object + let auth = match password { + Some(serde_json::Value::String(pw)) => serde_json::json!({ + "kind": "password", + "password": pw + }), + _ => serde_json::json!({ + "kind": "password" + }), + }; + + new_proto.insert("auth".to_string(), auth); + + let mut new_options = options_obj.clone(); + new_options.insert(kind_key.to_string(), serde_json::Value::Object(new_proto)); + + let mut model: target::ActiveModel = t.into(); + model.options = Set(serde_json::Value::Object(new_options)); + model.update(db).await?; + } + + Ok(()) + } + + #[allow(clippy::panic, reason = "dev only")] + async fn down(&self, _manager: &SchemaManager) -> Result<(), DbErr> { + panic!("This migration is irreversible"); + } +} diff --git a/warpgate-db-migrations/src/m00038_fix_target_auth_tags.rs b/warpgate-db-migrations/src/m00038_fix_target_auth_tags.rs new file mode 100644 index 000000000..87af2e836 --- /dev/null +++ b/warpgate-db-migrations/src/m00038_fix_target_auth_tags.rs @@ -0,0 +1,121 @@ +use sea_orm::{ActiveModelTrait, EntityTrait, Set}; +use sea_orm_migration::prelude::*; + +mod target { + use sea_orm::entity::prelude::*; + + #[derive(Debug, Clone, PartialEq, Eq, EnumIter, DeriveActiveEnum)] + #[sea_orm(rs_type = "String", db_type = "String(StringLen::None)")] + pub enum TargetKind { + #[sea_orm(string_value = "http")] + Http, + #[sea_orm(string_value = "kubernetes")] + Kubernetes, + #[sea_orm(string_value = "mysql")] + MySql, + #[sea_orm(string_value = "postgres")] + Postgres, + #[sea_orm(string_value = "ssh")] + Ssh, + #[sea_orm(string_value = "web_admin")] + WebAdmin, + } + + #[derive(Clone, Debug, PartialEq, Eq, DeriveEntityModel)] + #[sea_orm(table_name = "targets")] + pub struct Model { + #[sea_orm(primary_key, auto_increment = false)] + pub id: Uuid, + pub name: String, + pub kind: TargetKind, + pub options: serde_json::Value, + } + + #[derive(Copy, Clone, Debug, EnumIter, DeriveRelation)] + pub enum Relation {} + + impl ActiveModelBehavior for ActiveModel {} +} + +pub struct Migration; + +impl MigrationName for Migration { + fn name(&self) -> &str { + "m00038_fix_target_auth_tags" + } +} + +#[async_trait::async_trait] +impl MigrationTrait for Migration { + async fn up(&self, manager: &SchemaManager) -> Result<(), DbErr> { + let db = manager.get_connection(); + + let targets = target::Entity::find().all(db).await?; + + for t in targets { + let Some(options_obj) = t.options.as_object() else { + continue; + }; + + // Options are wrapped under a protocol key, e.g. {"ssh": {"auth": {}, ...}} + let kind_key = match t.kind { + target::TargetKind::Ssh => "ssh", + target::TargetKind::Kubernetes => "kubernetes", + _ => continue, + }; + + let Some(proto_obj) = options_obj.get(kind_key).and_then(|v| v.as_object()) else { + continue; + }; + + let Some(auth) = proto_obj.get("auth").and_then(|v| v.as_object()) else { + continue; + }; + + // Skip if auth already has a kind tag + if auth.contains_key("kind") { + continue; + } + + let kind_value = match t.kind { + target::TargetKind::Ssh => { + if auth.contains_key("password") { + "password" + } else { + "publickey" + } + } + target::TargetKind::Kubernetes => { + if auth.contains_key("token") { + "token" + } else { + "certificate" + } + } + _ => continue, + }; + + let mut new_proto = proto_obj.clone(); + let mut new_auth = auth.clone(); + new_auth.insert( + "kind".to_string(), + serde_json::Value::String(kind_value.to_string()), + ); + new_proto.insert("auth".to_string(), serde_json::Value::Object(new_auth)); + + let mut new_options = options_obj.clone(); + new_options.insert(kind_key.to_string(), serde_json::Value::Object(new_proto)); + + let mut model: target::ActiveModel = t.into(); + model.options = Set(serde_json::Value::Object(new_options)); + model.update(db).await?; + } + + Ok(()) + } + + #[allow(clippy::panic, reason = "dev only")] + async fn down(&self, _manager: &SchemaManager) -> Result<(), DbErr> { + panic!("This migration is irreversible"); + } +} diff --git a/warpgate-protocol-http/Cargo.toml b/warpgate-protocol-http/Cargo.toml index 46100410f..a5321b66d 100644 --- a/warpgate-protocol-http/Cargo.toml +++ b/warpgate-protocol-http/Cargo.toml @@ -25,6 +25,7 @@ tokio.workspace = true tokio-tungstenite.workspace = true tracing.workspace = true warpgate-admin = { path = "../warpgate-admin", default-features = false } +warpgate-aws = { path = "../warpgate-aws", default-features = false } warpgate-common = { path = "../warpgate-common", default-features = false } warpgate-common-http = { path = "../warpgate-common-http" } warpgate-ca = { path = "../warpgate-ca", default-features = false } diff --git a/warpgate-protocol-http/src/api/info.rs b/warpgate-protocol-http/src/api/info.rs index 09abc479f..1ba8298c5 100644 --- a/warpgate-protocol-http/src/api/info.rs +++ b/warpgate-protocol-http/src/api/info.rs @@ -88,6 +88,7 @@ pub struct Info { has_ldap: bool, setup_state: Option, admin_permissions: Option, + running_on_ec2: Option, } #[derive(ApiResponse)] @@ -290,6 +291,11 @@ impl Api { setup_state, has_ldap: auth_ctx.is_some() && has_ldap, admin_permissions, + running_on_ec2: if auth_ctx.is_some() { + Some(warpgate_aws::check_ec2().await) + } else { + None + }, }))) } } diff --git a/warpgate-protocol-kubernetes/Cargo.toml b/warpgate-protocol-kubernetes/Cargo.toml index f21b7a04e..3b914971f 100644 --- a/warpgate-protocol-kubernetes/Cargo.toml +++ b/warpgate-protocol-kubernetes/Cargo.toml @@ -31,6 +31,7 @@ time.workspace = true tracing.workspace = true url = { version = "2.0", default-features = false } uuid.workspace = true +warpgate-aws = { path = "../warpgate-aws", default-features = false } warpgate-common = { path = "../warpgate-common", default-features = false } warpgate-common-http = { path = "../warpgate-common-http", default-features = false } warpgate-core = { path = "../warpgate-core", default-features = false } diff --git a/warpgate-protocol-kubernetes/src/server/auth.rs b/warpgate-protocol-kubernetes/src/server/auth.rs index ce473d5ab..b17505761 100644 --- a/warpgate-protocol-kubernetes/src/server/auth.rs +++ b/warpgate-protocol-kubernetes/src/server/auth.rs @@ -3,6 +3,7 @@ use poem::Request; use sea_orm::{ActiveModelTrait, ColumnTrait, EntityTrait, QueryFilter, Set}; use time::OffsetDateTime; use tracing::{debug, warn}; +use warpgate_aws::EksClusterInfo; use warpgate_ca::{deserialize_certificate, serialize_certificate_serial}; use warpgate_common::auth::AuthStateUserInfo; use warpgate_common::{Target, TargetKubernetesOptions, TargetOptions, User}; @@ -112,7 +113,7 @@ pub async fn authenticate_and_get_target( )) } -pub fn create_authenticated_client( +pub async fn create_authenticated_client( k8s_options: &TargetKubernetesOptions, _auth_user: Option<&String>, _services: &Services, @@ -163,6 +164,25 @@ pub fn create_authenticated_client( .context("Invalid client certificate/key for Kubernetes upstream")?; client_builder = client_builder.identity(identity); } + warpgate_common::KubernetesTargetAuth::IamRole(_) => { + // EKS IAM role authentication: generate a token from the cluster URL + let EksClusterInfo { name, region } = + warpgate_aws::find_eks_cluster_by_url(&k8s_options.cluster_url) + .await + .context("EKS cluster lookup")?; + + let token = warpgate_aws::generate_eks_token(&name, ®ion) + .await + .context("EKS token generation")?; + + let mut headers = reqwest::header::HeaderMap::new(); + headers.insert( + reqwest::header::AUTHORIZATION, + reqwest::header::HeaderValue::from_str(&format!("Bearer {token}")) + .context("setting Authorization header for EKS token")?, + ); + client_builder = client_builder.default_headers(headers); + } } Ok(client_builder) diff --git a/warpgate-protocol-kubernetes/src/server/handlers.rs b/warpgate-protocol-kubernetes/src/server/handlers.rs index ac5da5798..41754eb15 100644 --- a/warpgate-protocol-kubernetes/src/server/handlers.rs +++ b/warpgate-protocol-kubernetes/src/server/handlers.rs @@ -144,7 +144,8 @@ async fn _handle_normal_request_inner( services: &Services, ) -> Result { let client = - create_authenticated_client(k8s_options, Some(&user_info.username.clone()), services)? + create_authenticated_client(k8s_options, Some(&user_info.username.clone()), services) + .await? .build() .context("building reqwest client")?; @@ -154,6 +155,7 @@ async fn _handle_normal_request_inner( match &k8s_options.auth { warpgate_common::KubernetesTargetAuth::Token(_) => "Token", warpgate_common::KubernetesTargetAuth::Certificate(_) => "Certificate", + warpgate_common::KubernetesTargetAuth::IamRole(_) => "IamRole", } ); @@ -387,7 +389,8 @@ async fn _handle_websocket_request_inner( } let client = - create_authenticated_client(k8s_options, Some(&user_info.username.clone()), services)? + create_authenticated_client(k8s_options, Some(&user_info.username.clone()), services) + .await? .http1_only() .build()?; diff --git a/warpgate-protocol-mysql/Cargo.toml b/warpgate-protocol-mysql/Cargo.toml index 6f33ad873..031e3ab04 100644 --- a/warpgate-protocol-mysql/Cargo.toml +++ b/warpgate-protocol-mysql/Cargo.toml @@ -6,6 +6,7 @@ version = "0.22.0-beta.5" publish = false [dependencies] +warpgate-aws = { path = "../warpgate-aws", default-features = false } warpgate-common = { path = "../warpgate-common", default-features = false } warpgate-tls = { path = "../warpgate-tls", default-features = false } warpgate-core = { path = "../warpgate-core", default-features = false } diff --git a/warpgate-protocol-mysql/src/client.rs b/warpgate-protocol-mysql/src/client.rs index 2767827c5..75b28953b 100644 --- a/warpgate-protocol-mysql/src/client.rs +++ b/warpgate-protocol-mysql/src/client.rs @@ -3,7 +3,7 @@ use std::sync::Arc; use bytes::BytesMut; use tokio::net::TcpStream; use tracing::{debug, info, trace, warn}; -use warpgate_common::TargetMySqlOptions; +use warpgate_common::{TargetMySqlOptions, WarpgateError}; use warpgate_database_protocols::io::Decode; use warpgate_database_protocols::mysql::protocol::auth::AuthPlugin; use warpgate_database_protocols::mysql::protocol::connect::{ @@ -117,6 +117,16 @@ impl MySqlClient { username: target.username.clone(), }; + // Resolve the effective password (may be an IAM-generated token or legacy field) + let effective_password = match &target.effective_auth() { + warpgate_common::DatabaseTargetAuth::Password(auth) => auth.password.clone(), + warpgate_common::DatabaseTargetAuth::IamRole(_) => { + warpgate_aws::generate_rds_auth_token(&target.host, target.port, &target.username) + .await + .map_err(WarpgateError::Aws)? + } + }; + if handshake.auth_plugin == Some(AuthPlugin::MySqlNativePassword) { let scramble_bytes = [ &handshake.auth_plugin_data.first_ref()[..], @@ -131,12 +141,9 @@ impl MySqlClient { response.auth_plugin = Some(AuthPlugin::MySqlNativePassword); response.auth_response = Some( BytesMut::from( - compute_auth_challenge_response( - scramble, - target.password.as_deref().unwrap_or(""), - ) - .map_err(MySqlError::other)? - .as_bytes(), + compute_auth_challenge_response(scramble, &effective_password) + .map_err(MySqlError::other)? + .as_bytes(), ) .freeze(), ); diff --git a/warpgate-protocol-postgres/Cargo.toml b/warpgate-protocol-postgres/Cargo.toml index d1d9bee24..bfca664e9 100644 --- a/warpgate-protocol-postgres/Cargo.toml +++ b/warpgate-protocol-postgres/Cargo.toml @@ -6,6 +6,7 @@ version = "0.22.0-beta.5" publish = false [dependencies] +warpgate-aws = { path = "../warpgate-aws", default-features = false } warpgate-common = { path = "../warpgate-common", default-features = false } warpgate-tls = { path = "../warpgate-tls", default-features = false } warpgate-core = { path = "../warpgate-core", default-features = false } diff --git a/warpgate-protocol-postgres/src/client.rs b/warpgate-protocol-postgres/src/client.rs index fb912b355..5d1c44144 100644 --- a/warpgate-protocol-postgres/src/client.rs +++ b/warpgate-protocol-postgres/src/client.rs @@ -9,7 +9,7 @@ use rsasl::prelude::{Mechname, SASLClient}; use tokio::net::TcpStream; use tokio_rustls::client::TlsStream; use tracing::{debug, info, warn}; -use warpgate_common::TargetPostgresOptions; +use warpgate_common::{TargetPostgresOptions, WarpgateError}; use warpgate_tls::{configure_tls_connector, TlsMode}; use crate::error::PostgresError; @@ -120,18 +120,26 @@ impl PostgresClient { stream.push(startup)?; stream.flush().await?; + // Resolve effective password (may be an IAM-generated token or legacy field) + let effective_password = match &target.effective_auth() { + warpgate_common::DatabaseTargetAuth::Password(auth) => auth.password.clone(), + warpgate_common::DatabaseTargetAuth::IamRole(_) => { + let token = warpgate_aws::generate_rds_auth_token( + &target.host, + target.port, + &target.username, + ) + .await + .map_err(WarpgateError::Aws)?; + token + } + }; + loop { let Some(payload) = stream.recv::().await? else { return Err(PostgresError::Eof); }; - let get_password = || { - target - .password - .as_ref() - .ok_or(PostgresError::PasswordRequired) - }; - match payload.0 { PgWireBackendMessage::ErrorResponse(err) => { return Err(PostgresError::from(err)); @@ -142,17 +150,15 @@ impl PostgresClient { break; } pgwire::messages::startup::Authentication::CleartextPassword => { - let password = get_password()?; let password_message = - pgwire::messages::startup::Password::new(password.into()); + pgwire::messages::startup::Password::new(effective_password.clone()); stream.push(password_message)?; stream.flush().await?; } pgwire::messages::startup::Authentication::MD5Password(scramble) => { - let password = get_password()?; let hashed = pgwire::api::auth::md5pass::hash_md5_password( &target.username, - password, + &effective_password, &scramble, ); let password_message = pgwire::messages::startup::Password::new(hashed); @@ -160,9 +166,13 @@ impl PostgresClient { stream.flush().await?; } pgwire::messages::startup::Authentication::SASL(mechanisms) => { - let password = get_password()?; - Self::run_sasl_auth(&mut stream, mechanisms, &target.username, password) - .await?; + Self::run_sasl_auth( + &mut stream, + mechanisms, + &target.username, + &effective_password, + ) + .await?; } x => { return Err(PostgresError::ProtocolError(format!( diff --git a/warpgate-protocol-postgres/src/error.rs b/warpgate-protocol-postgres/src/error.rs index c9d6c8e92..8b9e3312e 100644 --- a/warpgate-protocol-postgres/src/error.rs +++ b/warpgate-protocol-postgres/src/error.rs @@ -37,8 +37,6 @@ pub enum PostgresError { Sasl(#[from] SASLError), #[error("SASL session: {0}")] SaslSession(#[from] SessionError), - #[error("Password is required for authentication")] - PasswordRequired, #[error(transparent)] Warpgate(#[from] WarpgateError), #[error(transparent)] diff --git a/warpgate-protocol-ssh/Cargo.toml b/warpgate-protocol-ssh/Cargo.toml index b6e21e8ba..19da9a4d8 100644 --- a/warpgate-protocol-ssh/Cargo.toml +++ b/warpgate-protocol-ssh/Cargo.toml @@ -23,6 +23,7 @@ time = { version = "0.3", default-features = false } tokio.workspace = true tracing.workspace = true uuid.workspace = true +warpgate-aws = { path = "../warpgate-aws", default-features = false } warpgate-common = { path = "../warpgate-common", default-features = false } warpgate-core = { path = "../warpgate-core", default-features = false } warpgate-db-entities = { path = "../warpgate-db-entities", default-features = false } diff --git a/warpgate-protocol-ssh/src/client/mod.rs b/warpgate-protocol-ssh/src/client/mod.rs index eb41db7ee..3f15167be 100644 --- a/warpgate-protocol-ssh/src/client/mod.rs +++ b/warpgate-protocol-ssh/src/client/mod.rs @@ -23,13 +23,14 @@ use tokio::sync::{oneshot, Mutex}; use tokio::task::JoinHandle; use tracing::*; use uuid::Uuid; +use warpgate_aws::AwsError; use warpgate_common::{SSHTargetAuth, SessionId, TargetSSHOptions}; use warpgate_core::Services; use self::handler::ClientHandlerEvent; use super::{ChannelOperation, DirectTCPIPParams}; use crate::client::handler::ClientHandlerError; -use crate::{load_keys, ForwardedStreamlocalParams, ForwardedTcpIpParams}; +use crate::{load_keys, load_preferred_key, ForwardedStreamlocalParams, ForwardedTcpIpParams}; #[derive(Debug, thiserror::Error)] pub enum ConnectionError { @@ -50,6 +51,9 @@ pub enum ConnectionError { #[error(transparent)] Ssh(#[from] russh::Error), + #[error("AWS: {0}")] + Aws(#[from] AwsError), + #[error("Could not resolve address")] Resolve, @@ -621,6 +625,50 @@ impl RemoteClient { auth_error_msg = Some("Public key authentication was rejected by the SSH target".into()); } } + SSHTargetAuth::IamRole(_) => { + let instance_info = warpgate_aws::find_instance_by_ip(&ssh_options.host).await?; + + let key = load_preferred_key( + &*self.services.config.lock().await, + &self.services.global_params, + "client" + )?; + + let pub_key_str = key.public_key().to_openssh().map_err(russh::Error::from)?; + + // Push the public key via EC2 Instance Connect + warpgate_aws::send_ssh_public_key( + &instance_info.instance_id, + &instance_info.availability_zone, + &instance_info.region, + &ssh_options.username, + &pub_key_str, + ).await?; + + // Now authenticate with this key (key is valid for 60 seconds) + let key = Arc::new(key.clone()); + let best_hash = session.best_supported_rsa_hash().await?.flatten(); + let response = session + .authenticate_publickey( + ssh_options.username.clone(), + PrivateKeyWithHashAlg::new(key.clone(), best_hash), + ) + .await?; + + auth_result = self._handle_auth_result( + &mut session, + ssh_options.username.clone(), + response + ).await.unwrap_or(false); + + if auth_result { + debug!(username=&ssh_options.username[..], "Authenticated via EC2 Instance Connect"); + } + + if !auth_result { + auth_error_msg = Some("EC2 Instance Connect authentication was rejected by the SSH target".into()); + } + } } if !auth_result { diff --git a/warpgate-protocol-ssh/src/keys.rs b/warpgate-protocol-ssh/src/keys.rs index 99c045b3c..8c9c24ab1 100644 --- a/warpgate-protocol-ssh/src/keys.rs +++ b/warpgate-protocol-ssh/src/keys.rs @@ -57,3 +57,12 @@ pub fn load_keys( load_secret_key(path.join(format!("{prefix}-rsa")), None)?, ]) } + +pub fn load_preferred_key( + config: &WarpgateConfig, + params: &GlobalParams, + prefix: &str, +) -> Result { + let path = get_keys_path(config, params); + load_secret_key(path.join(format!("{prefix}-ed25519")), None) +} diff --git a/warpgate-web/src/admin/config/targets/CreateTarget.svelte b/warpgate-web/src/admin/config/targets/CreateTarget.svelte index 1962f99d9..984d0a60a 100644 --- a/warpgate-web/src/admin/config/targets/CreateTarget.svelte +++ b/warpgate-web/src/admin/config/targets/CreateTarget.svelte @@ -48,7 +48,9 @@ verify: true, }, username: 'root', - password: '', + auth: { + kind: 'Password' as const, + }, }, Postgres: { kind: TargetKind.Postgres, @@ -59,7 +61,9 @@ verify: true, }, username: 'postgres', - password: '', + auth: { + kind: 'Password' as const, + }, }, Kubernetes: { kind: TargetKind.Kubernetes, diff --git a/warpgate-web/src/admin/config/targets/Target.svelte b/warpgate-web/src/admin/config/targets/Target.svelte index 577bbd542..5026f289d 100644 --- a/warpgate-web/src/admin/config/targets/Target.svelte +++ b/warpgate-web/src/admin/config/targets/Target.svelte @@ -83,7 +83,7 @@
{#if target} - connectionsInstructionsModalOpen = false}> + connectionsInstructionsModalOpen = false} size="lg"> Access instructions @@ -217,12 +217,23 @@
- - + +
+ {#if target.options.auth.kind === 'Password'} + + + + {/if} + {/if} @@ -236,6 +247,9 @@ diff --git a/warpgate-web/src/admin/config/targets/ssh/Options.svelte b/warpgate-web/src/admin/config/targets/ssh/Options.svelte index 1ab476c28..a01f86f11 100644 --- a/warpgate-web/src/admin/config/targets/ssh/Options.svelte +++ b/warpgate-web/src/admin/config/targets/ssh/Options.svelte @@ -6,6 +6,7 @@ import TargetSshHostKeyChecker from './KeyChecker.svelte' import Alert from 'common/sveltestrap-s5-ports/Alert.svelte' import { adminPermissions } from 'admin/lib/store' + import { serverInfo } from 'gateway/lib/store' interface Props { id: string, @@ -62,6 +63,9 @@ {#if options.auth.kind === 'PublicKey'} diff --git a/warpgate-web/src/admin/lib/openapi-schema.json b/warpgate-web/src/admin/lib/openapi-schema.json index e06559b8a..215e94ed2 100644 --- a/warpgate-web/src/admin/lib/openapi-schema.json +++ b/warpgate-web/src/admin/lib/openapi-schema.json @@ -2,7 +2,7 @@ "openapi": "3.0.0", "info": { "title": "Warpgate Web Admin", - "version": "v0.22.0-beta.4-17-g75b5fc29-modified" + "version": "v0.22.0-beta.5-2-g1fc7f399-modified" }, "servers": [ { @@ -4182,6 +4182,81 @@ "WebUserApproval" ] }, + "DatabaseTargetAuth": { + "type": "object", + "oneOf": [ + { + "$ref": "#/components/schemas/DatabaseTargetAuth_DatabaseTargetPasswordAuth" + }, + { + "$ref": "#/components/schemas/DatabaseTargetAuth_DatabaseTargetIamRoleAuth" + } + ], + "discriminator": { + "propertyName": "kind", + "mapping": { + "Password": "#/components/schemas/DatabaseTargetAuth_DatabaseTargetPasswordAuth", + "IamRole": "#/components/schemas/DatabaseTargetAuth_DatabaseTargetIamRoleAuth" + } + } + }, + "DatabaseTargetAuth_DatabaseTargetIamRoleAuth": { + "allOf": [ + { + "type": "object", + "required": [ + "kind" + ], + "properties": { + "kind": { + "type": "string", + "enum": [ + "IamRole" + ], + "example": "IamRole" + } + } + }, + { + "$ref": "#/components/schemas/DatabaseTargetIamRoleAuth" + } + ] + }, + "DatabaseTargetAuth_DatabaseTargetPasswordAuth": { + "allOf": [ + { + "type": "object", + "required": [ + "kind" + ], + "properties": { + "kind": { + "type": "string", + "enum": [ + "Password" + ], + "example": "Password" + } + } + }, + { + "$ref": "#/components/schemas/DatabaseTargetPasswordAuth" + } + ] + }, + "DatabaseTargetIamRoleAuth": { + "type": "object", + "title": "DatabaseTargetIamRoleAuth" + }, + "DatabaseTargetPasswordAuth": { + "type": "object", + "title": "DatabaseTargetPasswordAuth", + "properties": { + "password": { + "type": "string" + } + } + }, "ExistingCertificateCredential": { "type": "object", "title": "ExistingCertificateCredential", @@ -4413,13 +4488,17 @@ }, { "$ref": "#/components/schemas/KubernetesTargetAuth_KubernetesTargetCertificateAuth" + }, + { + "$ref": "#/components/schemas/KubernetesTargetAuth_KubernetesTargetIamRoleAuth" } ], "discriminator": { "propertyName": "kind", "mapping": { "Token": "#/components/schemas/KubernetesTargetAuth_KubernetesTargetTokenAuth", - "Certificate": "#/components/schemas/KubernetesTargetAuth_KubernetesTargetCertificateAuth" + "Certificate": "#/components/schemas/KubernetesTargetAuth_KubernetesTargetCertificateAuth", + "IamRole": "#/components/schemas/KubernetesTargetAuth_KubernetesTargetIamRoleAuth" } } }, @@ -4445,6 +4524,28 @@ } ] }, + "KubernetesTargetAuth_KubernetesTargetIamRoleAuth": { + "allOf": [ + { + "type": "object", + "required": [ + "kind" + ], + "properties": { + "kind": { + "type": "string", + "enum": [ + "IamRole" + ], + "example": "IamRole" + } + } + }, + { + "$ref": "#/components/schemas/KubernetesTargetIamRoleAuth" + } + ] + }, "KubernetesTargetAuth_KubernetesTargetTokenAuth": { "allOf": [ { @@ -4483,6 +4584,10 @@ } } }, + "KubernetesTargetIamRoleAuth": { + "type": "object", + "title": "KubernetesTargetIamRoleAuth" + }, "KubernetesTargetTokenAuth": { "type": "object", "title": "KubernetesTargetTokenAuth", @@ -4926,16 +5031,42 @@ }, { "$ref": "#/components/schemas/SSHTargetAuth_SshTargetPublicKeyAuth" + }, + { + "$ref": "#/components/schemas/SSHTargetAuth_SshTargetIamRoleAuth" } ], "discriminator": { "propertyName": "kind", "mapping": { "Password": "#/components/schemas/SSHTargetAuth_SshTargetPasswordAuth", - "PublicKey": "#/components/schemas/SSHTargetAuth_SshTargetPublicKeyAuth" + "PublicKey": "#/components/schemas/SSHTargetAuth_SshTargetPublicKeyAuth", + "IamRole": "#/components/schemas/SSHTargetAuth_SshTargetIamRoleAuth" } } }, + "SSHTargetAuth_SshTargetIamRoleAuth": { + "allOf": [ + { + "type": "object", + "required": [ + "kind" + ], + "properties": { + "kind": { + "type": "string", + "enum": [ + "IamRole" + ], + "example": "IamRole" + } + } + }, + { + "$ref": "#/components/schemas/SshTargetIamRoleAuth" + } + ] + }, "SSHTargetAuth_SshTargetPasswordAuth": { "allOf": [ { @@ -5016,6 +5147,10 @@ } } }, + "SshTargetIamRoleAuth": { + "type": "object", + "title": "SshTargetIamRoleAuth" + }, "SshTargetPasswordAuth": { "type": "object", "title": "SshTargetPasswordAuth", @@ -5193,6 +5328,7 @@ "host", "port", "username", + "auth", "tls" ], "properties": { @@ -5206,8 +5342,8 @@ "username": { "type": "string" }, - "password": { - "type": "string" + "auth": { + "$ref": "#/components/schemas/DatabaseTargetAuth" }, "tls": { "$ref": "#/components/schemas/Tls" @@ -5364,6 +5500,7 @@ "host", "port", "username", + "auth", "tls" ], "properties": { @@ -5377,8 +5514,8 @@ "username": { "type": "string" }, - "password": { - "type": "string" + "auth": { + "$ref": "#/components/schemas/DatabaseTargetAuth" }, "tls": { "$ref": "#/components/schemas/Tls" diff --git a/warpgate-web/src/gateway/lib/openapi-schema.json b/warpgate-web/src/gateway/lib/openapi-schema.json index 38f44084c..a53db87a5 100644 --- a/warpgate-web/src/gateway/lib/openapi-schema.json +++ b/warpgate-web/src/gateway/lib/openapi-schema.json @@ -2,7 +2,7 @@ "openapi": "3.0.0", "info": { "title": "Warpgate HTTP proxy", - "version": "v0.22.0-beta.4-17-g75b5fc29-modified" + "version": "v0.22.0-beta.5-2-g1fc7f399-modified" }, "servers": [ { @@ -1216,6 +1216,9 @@ }, "admin_permissions": { "$ref": "#/components/schemas/AdminPermissions" + }, + "running_on_ec2": { + "type": "boolean" } } }, diff --git a/warpgate/src/commands/setup.rs b/warpgate/src/commands/setup.rs index fe961e4ce..22202440b 100644 --- a/warpgate/src/commands/setup.rs +++ b/warpgate/src/commands/setup.rs @@ -8,7 +8,10 @@ use std::path::{Path, PathBuf}; use anyhow::{Context, Result}; use dialoguer::theme::ColorfulTheme; use rcgen::generate_simple_self_signed; +use sea_orm::ActiveValue::Set; +use sea_orm::{ActiveModelTrait, EntityTrait}; use tracing::{error, info}; +use uuid::Uuid; use warpgate_common::helpers::fs::{secure_directory, secure_file}; use warpgate_common::version::warpgate_version; use warpgate_common::{ @@ -16,6 +19,8 @@ use warpgate_common::{ Secret, SshConfig, WarpgateConfigStore, }; use warpgate_core::consts::{BUILTIN_ADMIN_ROLE_NAME, BUILTIN_ADMIN_USERNAME}; +use warpgate_core::db::connect_to_db; +use warpgate_db_entities::{Role, User, UserRoleAssignment}; use crate::commands::common::{assert_interactive_terminal, is_docker}; use crate::config::load_config; @@ -340,6 +345,30 @@ pub async fn command(cli: &Cli, params: &GlobalParams) -> Result<()> { ) .await?; + let db = connect_to_db(&config, params).await?; + + #[allow(clippy::expect_used)] + let user = User::Entity::find() + .one(&db) + .await? + .expect("Admin user should exist"); + + let access_role = Role::ActiveModel { + id: Set(Uuid::new_v4()), + name: Set(BUILTIN_ADMIN_USERNAME.to_string()), + ..Default::default() + } + .insert(&db) + .await?; + + UserRoleAssignment::ActiveModel { + user_id: Set(user.id), + role_id: Set(access_role.id), + ..Default::default() + } + .insert(&db) + .await?; + { info!("Generating a TLS certificate"); let cert = generate_simple_self_signed(vec![ From 7785f929e02ec972af82242e79eb0e6516335997 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Mon, 13 Apr 2026 10:34:35 +0200 Subject: [PATCH 014/556] Bump debian from bullseye-20260316 to bullseye-20260406 in /docker (#1845) Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> --- docker/Dockerfile | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docker/Dockerfile b/docker/Dockerfile index 36004ee27..e8296c3ca 100644 --- a/docker/Dockerfile +++ b/docker/Dockerfile @@ -26,7 +26,7 @@ RUN just npm ci \ && just npm run build \ && cargo build --features mysql,postgres --release -FROM debian:bullseye-20260316@sha256:943d97fa707482c24e1bc2bdd0b0adc45f75eb345c61dc4272c4157f9a2cc9cc +FROM debian:bullseye-20260406@sha256:bf53effcacca31b60ce97dabc67578f37e43075d716dc90804d3da3a80d2996c LABEL maintainer=heywoodlh ARG USER_ID=1000 From 4e7cc0988e7495a9866fed8050790a069d0a9262 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Mon, 13 Apr 2026 10:34:47 +0200 Subject: [PATCH 015/556] Bump @nestjs/core and @openapitools/openapi-generator-cli in /warpgate-web (#1861) Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> --- warpgate-web/package-lock.json | 54 +++++++++++++++++----------------- warpgate-web/package.json | 2 +- 2 files changed, 28 insertions(+), 28 deletions(-) diff --git a/warpgate-web/package-lock.json b/warpgate-web/package-lock.json index a9f5401c7..8357f88c7 100644 --- a/warpgate-web/package-lock.json +++ b/warpgate-web/package-lock.json @@ -18,7 +18,7 @@ "@fortawesome/free-brands-svg-icons": "^7.2.0", "@fortawesome/free-regular-svg-icons": "^7.2.0", "@fortawesome/free-solid-svg-icons": "^7.2.0", - "@openapitools/openapi-generator-cli": "^2.30.2", + "@openapitools/openapi-generator-cli": "^2.31.1", "@otplib/plugin-base32-enc-dec": "^12.0.1", "@otplib/plugin-crypto-js": "^12.0.1", "@otplib/preset-browser": "^12.0.1", @@ -87,9 +87,9 @@ } }, "node_modules/@borewit/text-codec": { - "version": "0.2.1", - "resolved": "https://registry.npmjs.org/@borewit/text-codec/-/text-codec-0.2.1.tgz", - "integrity": "sha512-k7vvKPbf7J2fZ5klGRD9AeKfUvojuZIQ3BT5u7Jfv+puwXkUBUT5PVyMDfJZpy30CBDXGMgw7fguK/lpOMBvgw==", + "version": "0.2.2", + "resolved": "https://registry.npmjs.org/@borewit/text-codec/-/text-codec-0.2.2.tgz", + "integrity": "sha512-DDaRehssg1aNrH4+2hnj1B7vnUGEjU6OIlyRdkMd0aUdIUvKXrJfXsy8LVtXAy7DRvYVluWbMspsRhz2lcW0mQ==", "dev": true, "license": "MIT", "funding": { @@ -1078,13 +1078,13 @@ } }, "node_modules/@nestjs/common": { - "version": "11.1.16", - "resolved": "https://registry.npmjs.org/@nestjs/common/-/common-11.1.16.tgz", - "integrity": "sha512-JSIeW+USuMJkkcNbiOdcPkVCeI3TSnXstIVEPpp3HiaKnPRuSbUUKm9TY9o/XpIcPHWUOQItAtC5BiAwFdVITQ==", + "version": "11.1.17", + "resolved": "https://registry.npmjs.org/@nestjs/common/-/common-11.1.17.tgz", + "integrity": "sha512-hLODw5Abp8OQgA+mUO4tHou4krKgDtUcM9j5Ihxncst9XeyxYBTt2bwZm4e4EQr5E352S4Fyy6V3iFx9ggxKAg==", "dev": true, "license": "MIT", "dependencies": { - "file-type": "21.3.0", + "file-type": "21.3.2", "iterare": "1.2.1", "load-esm": "1.0.3", "tslib": "2.8.1", @@ -1110,9 +1110,9 @@ } }, "node_modules/@nestjs/core": { - "version": "11.1.16", - "resolved": "https://registry.npmjs.org/@nestjs/core/-/core-11.1.16.tgz", - "integrity": "sha512-tXWXyCiqWthelJjrE0KLFjf0O98VEt+WPVx5CrqCf+059kIxJ8y1Vw7Cy7N4fwQafWNrmFL2AfN87DDMbVAY0w==", + "version": "11.1.18", + "resolved": "https://registry.npmjs.org/@nestjs/core/-/core-11.1.18.tgz", + "integrity": "sha512-wR3DtGyk/LUAiPtbXDuWJJwVkWElKBY0sqnTzf9d4uM3+X18FRZhK7WFc47czsIGOdWuRsMeLYV+1Z9dO4zDEQ==", "dev": true, "hasInstallScript": true, "license": "MIT", @@ -1120,7 +1120,7 @@ "@nuxt/opencollective": "0.4.1", "fast-safe-stringify": "2.1.1", "iterare": "1.2.1", - "path-to-regexp": "8.3.0", + "path-to-regexp": "8.4.2", "tslib": "2.8.1", "uid": "2.0.2" }, @@ -1208,19 +1208,19 @@ "license": "MIT" }, "node_modules/@openapitools/openapi-generator-cli": { - "version": "2.30.2", - "resolved": "https://registry.npmjs.org/@openapitools/openapi-generator-cli/-/openapi-generator-cli-2.30.2.tgz", - "integrity": "sha512-rGgLrY88f7/eTBc2wmehhcqQq7/1wEkNQUhvk1NF0nh/bCGGGRfzN6O4U2VHsREtshUT+IUaRoJwq4UeDrRXZQ==", + "version": "2.31.1", + "resolved": "https://registry.npmjs.org/@openapitools/openapi-generator-cli/-/openapi-generator-cli-2.31.1.tgz", + "integrity": "sha512-dPE+COjNLLTHFQ1lddUvpo+J8YQB1RD3/NVRJ3K+1hPZnyuxCURgOCmr7mXgHEyHmzWH8dKXWm/pD170iVR0vw==", "dev": true, "hasInstallScript": true, "license": "Apache-2.0", "dependencies": { "@inquirer/select": "1.3.3", "@nestjs/axios": "4.0.1", - "@nestjs/common": "11.1.16", - "@nestjs/core": "11.1.16", + "@nestjs/common": "11.1.17", + "@nestjs/core": "11.1.18", "@nuxtjs/opencollective": "0.3.2", - "axios": "^1.13.6", + "axios": "^1.14.0", "chalk": "4.1.2", "commander": "8.3.0", "compare-versions": "6.1.1", @@ -4470,9 +4470,9 @@ } }, "node_modules/file-type": { - "version": "21.3.0", - "resolved": "https://registry.npmjs.org/file-type/-/file-type-21.3.0.tgz", - "integrity": "sha512-8kPJMIGz1Yt/aPEwOsrR97ZyZaD1Iqm8PClb1nYFclUCkBi0Ma5IsYNQzvSFS9ib51lWyIw5mIT9rWzI/xjpzA==", + "version": "21.3.2", + "resolved": "https://registry.npmjs.org/file-type/-/file-type-21.3.2.tgz", + "integrity": "sha512-DLkUvGwep3poOV2wpzbHCOnSKGk1LzyXTv+aHFgN2VFl96wnp8YA9YjO2qPzg5PuL8q/SW9Pdi6WTkYOIh995w==", "dev": true, "license": "MIT", "dependencies": { @@ -6230,9 +6230,9 @@ } }, "node_modules/path-to-regexp": { - "version": "8.3.0", - "resolved": "https://registry.npmjs.org/path-to-regexp/-/path-to-regexp-8.3.0.tgz", - "integrity": "sha512-7jdwVIRtsP8MYpdXSwOS0YdD0Du+qOoF/AEPIt88PcCFrZCzx41oxku1jD88hZBwbNUIEfpqvuhjFaMAqMTWnA==", + "version": "8.4.2", + "resolved": "https://registry.npmjs.org/path-to-regexp/-/path-to-regexp-8.4.2.tgz", + "integrity": "sha512-qRcuIdP69NPm4qbACK+aDogI5CBDMi1jKe0ry5rSQJz8JVLsC7jV8XpiJjGRLLol3N+R5ihGYcrPLTno6pAdBA==", "dev": true, "license": "MIT", "funding": { @@ -7310,9 +7310,9 @@ } }, "node_modules/strtok3": { - "version": "10.3.4", - "resolved": "https://registry.npmjs.org/strtok3/-/strtok3-10.3.4.tgz", - "integrity": "sha512-KIy5nylvC5le1OdaaoCJ07L+8iQzJHGH6pWDuzS+d07Cu7n1MZ2x26P8ZKIWfbK02+XIL8Mp4RkWeqdUCrDMfg==", + "version": "10.3.5", + "resolved": "https://registry.npmjs.org/strtok3/-/strtok3-10.3.5.tgz", + "integrity": "sha512-ki4hZQfh5rX0QDLLkOCj+h+CVNkqmp/CMf8v8kZpkNVK6jGQooMytqzLZYUVYIZcFZ6yDB70EfD8POcFXiF5oA==", "dev": true, "license": "MIT", "dependencies": { diff --git a/warpgate-web/package.json b/warpgate-web/package.json index 7ac449108..0f1d76874 100644 --- a/warpgate-web/package.json +++ b/warpgate-web/package.json @@ -23,7 +23,7 @@ "@fortawesome/free-brands-svg-icons": "^7.2.0", "@fortawesome/free-regular-svg-icons": "^7.2.0", "@fortawesome/free-solid-svg-icons": "^7.2.0", - "@openapitools/openapi-generator-cli": "^2.30.2", + "@openapitools/openapi-generator-cli": "^2.31.1", "@otplib/plugin-base32-enc-dec": "^12.0.1", "@otplib/plugin-crypto-js": "^12.0.1", "@otplib/preset-browser": "^12.0.1", From 26b7e7260c0f2f6bf11b8d2abf02ee0b24a169e5 Mon Sep 17 00:00:00 2001 From: Lars <60571459+LarsSven@users.noreply.github.com> Date: Mon, 13 Apr 2026 12:56:47 +0200 Subject: [PATCH 016/556] Add support for disabling the injected menu (#1852) Co-authored-by: Eugene --- warpgate-admin/src/api/parameters.rs | 4 ++ warpgate-db-entities/src/Parameters.rs | 2 + warpgate-db-migrations/src/lib.rs | 2 + .../src/m00039_show_session_menu.rs | 45 +++++++++++++++++++ warpgate-protocol-http/src/proxy.rs | 18 ++++++-- .../src/admin/config/Parameters.svelte | 20 +++++++++ .../src/admin/lib/openapi-schema.json | 26 +++++++++-- .../src/gateway/lib/openapi-schema.json | 2 +- 8 files changed, 110 insertions(+), 9 deletions(-) create mode 100644 warpgate-db-migrations/src/m00039_show_session_menu.rs diff --git a/warpgate-admin/src/api/parameters.rs b/warpgate-admin/src/api/parameters.rs index b53f1aa02..9a0dc9cfe 100644 --- a/warpgate-admin/src/api/parameters.rs +++ b/warpgate-admin/src/api/parameters.rs @@ -21,6 +21,7 @@ struct ParameterValues { pub ssh_client_auth_password: bool, pub ssh_client_auth_keyboard_interactive: bool, pub minimize_password_login: bool, + pub show_session_menu: bool, } #[derive(Serialize, Object)] @@ -31,6 +32,7 @@ struct ParameterUpdate { pub ssh_client_auth_password: Option, pub ssh_client_auth_keyboard_interactive: Option, pub minimize_password_login: Option, + pub show_session_menu: Option, } #[derive(ApiResponse)] @@ -65,6 +67,7 @@ impl Api { ssh_client_auth_password: parameters.ssh_client_auth_password, ssh_client_auth_keyboard_interactive: parameters.ssh_client_auth_keyboard_interactive, minimize_password_login: parameters.minimize_password_login, + show_session_menu: parameters.show_session_menu, }))) } @@ -94,6 +97,7 @@ impl Api { .ssh_client_auth_keyboard_interactive .map_or(NotSet, Set); parameters.minimize_password_login = body.minimize_password_login.map_or(NotSet, Set); + parameters.show_session_menu = body.show_session_menu.map_or(NotSet, Set); Parameters::Entity::update(parameters).exec(&*db).await?; drop(db); diff --git a/warpgate-db-entities/src/Parameters.rs b/warpgate-db-entities/src/Parameters.rs index 61ceb2297..e4ca6cf13 100644 --- a/warpgate-db-entities/src/Parameters.rs +++ b/warpgate-db-entities/src/Parameters.rs @@ -17,6 +17,7 @@ pub struct Model { pub ssh_client_auth_password: bool, pub ssh_client_auth_keyboard_interactive: bool, pub minimize_password_login: bool, + pub show_session_menu: bool, } impl ActiveModelBehavior for ActiveModel {} @@ -39,6 +40,7 @@ impl Entity { ssh_client_auth_password: Set(true), ssh_client_auth_keyboard_interactive: Set(true), minimize_password_login: Set(false), + show_session_menu: Set(true), } .insert(db) .await diff --git a/warpgate-db-migrations/src/lib.rs b/warpgate-db-migrations/src/lib.rs index fecaaaf1c..71e82d0e4 100644 --- a/warpgate-db-migrations/src/lib.rs +++ b/warpgate-db-migrations/src/lib.rs @@ -40,6 +40,7 @@ mod m00035_ticket_user_target_id; mod m00036_user_role_expiry_history; mod m00037_database_target_auth; mod m00038_fix_target_auth_tags; +mod m00039_show_session_menu; pub struct Migrator; @@ -85,6 +86,7 @@ impl MigratorTrait for Migrator { Box::new(m00036_user_role_expiry_history::Migration), Box::new(m00037_database_target_auth::Migration), Box::new(m00038_fix_target_auth_tags::Migration), + Box::new(m00039_show_session_menu::Migration), ] } } diff --git a/warpgate-db-migrations/src/m00039_show_session_menu.rs b/warpgate-db-migrations/src/m00039_show_session_menu.rs new file mode 100644 index 000000000..4e39da525 --- /dev/null +++ b/warpgate-db-migrations/src/m00039_show_session_menu.rs @@ -0,0 +1,45 @@ +use sea_orm_migration::prelude::*; + +use crate::m00010_parameters::parameters; + +pub struct Migration; + +impl MigrationName for Migration { + fn name(&self) -> &str { + "m00037_show_session_menu" + } +} + +#[async_trait::async_trait] +impl MigrationTrait for Migration { + async fn up(&self, manager: &SchemaManager) -> Result<(), DbErr> { + manager + .alter_table( + Table::alter() + .table(parameters::Entity) + .add_column( + ColumnDef::new(Alias::new("show_session_menu")) + .boolean() + .not_null() + .default(true), + ) + .to_owned(), + ) + .await?; + + Ok(()) + } + + async fn down(&self, manager: &SchemaManager) -> Result<(), DbErr> { + manager + .alter_table( + Table::alter() + .table(parameters::Entity) + .drop_column(Alias::new("show_session_menu")) + .to_owned(), + ) + .await?; + + Ok(()) + } +} diff --git a/warpgate-protocol-http/src/proxy.rs b/warpgate-protocol-http/src/proxy.rs index a55e4859f..96fbf8e7f 100644 --- a/warpgate-protocol-http/src/proxy.rs +++ b/warpgate-protocol-http/src/proxy.rs @@ -305,7 +305,15 @@ pub async fn proxy_normal_request( let mut response: Response = "".into(); copy_client_response(&client_response, &mut response); - copy_client_body(client_response, &mut response).await?; + + let embed_session_menu = { + let db = ctx.services().db.lock().await; + warpgate_db_entities::Parameters::Entity::get(&db) + .await + .map(|p| p.show_session_menu) + .unwrap_or(true) + }; + copy_client_body(client_response, &mut response, embed_session_menu).await?; log_request_result( req.method(), @@ -321,10 +329,12 @@ pub async fn proxy_normal_request( async fn copy_client_body( client_response: reqwest::Response, response: &mut Response, + embed_session_menu: bool, ) -> Result<()> { - if response - .content_type() - .is_some_and(|c| c.starts_with("text/html")) + if embed_session_menu + && response + .content_type() + .is_some_and(|c| c.starts_with("text/html")) && response.status() == 200 { copy_client_body_and_embed(client_response, response).await?; diff --git a/warpgate-web/src/admin/config/Parameters.svelte b/warpgate-web/src/admin/config/Parameters.svelte index 1fd17fe09..b569ddb5e 100644 --- a/warpgate-web/src/admin/config/Parameters.svelte +++ b/warpgate-web/src/admin/config/Parameters.svelte @@ -111,6 +111,26 @@ Disabling password authentication can help prevent brute-force attacks. +

HTTP

+ + + Warpgate can inject a session menu into HTTP sessions, allowing users to log out or return back to the home page. + + {#if hasSsoProviders}

Login

- {#if $serverInfo?.runningOnEc2} @@ -228,9 +228,9 @@
- {#if target.options.auth.kind === 'Password'} + {#if target.options.auth!.kind === 'Password'} - + {/if} diff --git a/warpgate-web/src/admin/config/users/AllowedIpRangesEditor.svelte b/warpgate-web/src/admin/config/users/AllowedIpRangesEditor.svelte new file mode 100644 index 000000000..ff3bdc8a7 --- /dev/null +++ b/warpgate-web/src/admin/config/users/AllowedIpRangesEditor.svelte @@ -0,0 +1,73 @@ + + +
+ + + {#if ranges?.length} + {#each ranges as range, index (index)} +
+ { + if (ranges) { + ranges[index] = e.target.value + ranges = [...ranges] + } + }} + invalid={!!range?.trim() && !isValidCidr(range)} + /> + +
+ {#if range?.trim() && !isValidCidr(range)} + + Invalid CIDR notation. Use a format like 192.168.1.0/24 or 10.0.0.1/32. + + {/if} + {/each} + {/if} + + + If set, only connections from these IP ranges will be allowed. Use CIDR notation (e.g. 10.0.0.0/8, 192.168.1.0/24, or a single IP like 1.2.3.4/32). Leave empty to allow all IPs. + +
diff --git a/warpgate-web/src/admin/AuthPolicyEditor.svelte b/warpgate-web/src/admin/config/users/AuthPolicyEditor.svelte similarity index 99% rename from warpgate-web/src/admin/AuthPolicyEditor.svelte rename to warpgate-web/src/admin/config/users/AuthPolicyEditor.svelte index b53c196f1..9bfe8ce81 100644 --- a/warpgate-web/src/admin/AuthPolicyEditor.svelte +++ b/warpgate-web/src/admin/config/users/AuthPolicyEditor.svelte @@ -1,6 +1,6 @@
@@ -317,48 +320,6 @@ /> {/if} -
- - - - - {#if $serverInfo?.hasLdap} - - - {#if user.ldapServerId} - - {/if} - LDAP - - - - {#if user.ldapServerId} - - - Unlink from LDAP - - {:else} - - - Auto-link to LDAP - - {/if} - - - {/if} -
- - - - - - -

User roles

{#each allRoles as role (role.id)} @@ -451,13 +412,18 @@

Traffic

- + + +

Access restrictions

+
+ +
{/if} diff --git a/warpgate-web/src/admin/config/Users.svelte b/warpgate-web/src/admin/config/users/Users.svelte similarity index 98% rename from warpgate-web/src/admin/config/Users.svelte rename to warpgate-web/src/admin/config/users/Users.svelte index 931af4f9f..2b987c14f 100644 --- a/warpgate-web/src/admin/config/Users.svelte +++ b/warpgate-web/src/admin/config/users/Users.svelte @@ -1,7 +1,7 @@ @@ -111,6 +125,107 @@ Disabling password authentication can help prevent brute-force attacks. +

Self-service tickets

+ + + When enabled, authenticated users can request user-tied, time-limited access tickets from their profile page or via the API to a single target. + + + {#if parameters.ticketSelfServiceEnabled} + + + + + + + When disabled, users only see targets they already have role-based access to. + + + + + + Global default. Can be overridden per target. Examples: 30m, 8h, 1d, 2h30m. + + + + + { + const v = parseInt(e.currentTarget.value) + parameters!.ticketMaxUses = isNaN(v) ? undefined : v + update() + }} + /> + + + {/if} +

HTTP

diff --git a/warpgate-web/src/admin/config/targets/http/HeadersEditor.svelte b/warpgate-web/src/admin/config/targets/http/HeadersEditor.svelte index 6ecd36c59..489327e9c 100644 --- a/warpgate-web/src/admin/config/targets/http/HeadersEditor.svelte +++ b/warpgate-web/src/admin/config/targets/http/HeadersEditor.svelte @@ -77,8 +77,6 @@ }) -

Additional headers

- Headers are added to all requests forwarded to the target. Some headers are automatically set by Warpgate. diff --git a/warpgate-web/src/admin/config/users/CredentialEditor.svelte b/warpgate-web/src/admin/config/users/CredentialEditor.svelte index 1c7d46455..1643c01e4 100644 --- a/warpgate-web/src/admin/config/users/CredentialEditor.svelte +++ b/warpgate-web/src/admin/config/users/CredentialEditor.svelte @@ -290,7 +290,7 @@ } -
+

Credentials

{#if $adminPermissions.usersEdit} @@ -477,4 +477,15 @@ margin-left: .75rem; } } + + .header { + align-items: center; + } + + @media (max-width: 720px) { + .header { + flex-direction: column; + align-items: start; + } + } diff --git a/warpgate-web/src/admin/config/users/User.svelte b/warpgate-web/src/admin/config/users/User.svelte index fd260800d..8b7020635 100644 --- a/warpgate-web/src/admin/config/users/User.svelte +++ b/warpgate-web/src/admin/config/users/User.svelte @@ -19,6 +19,10 @@ import AdminRolePermissionsBadge from '../AdminRolePermissionsBadge.svelte' import Tooltip from 'common/sveltestrap-s5-ports/Tooltip.svelte' import { formatDistanceToNow } from 'date-fns' + import StickyActionBar from 'common/StickyActionBar.svelte' + import PageSummaryBar from 'common/PageSummaryBar.svelte' + import SectionedForm from 'admin/lib/SectionedForm.svelte' + import Section from 'admin/lib/Section.svelte' interface Props { params: { id: string }; @@ -266,191 +270,205 @@ -
+
{#if user} -
-
-

{user.username}

-
User
-
-
- -
- - - + {#if error} + error = null}>{error} + {/if} - {#if $serverInfo?.hasLdap} - - - {#if user.ldapServerId} - - {/if} - LDAP - - - - {#if user.ldapServerId} - - - Unlink from LDAP - - {:else} - - - Auto-link to LDAP - - {/if} - - - {/if} -
+ - - - + +
+
+ + + - {#if $adminPermissions.usersEdit} - - {/if} + {#if $serverInfo?.hasLdap} + + + {#if user.ldapServerId} + + {/if} + LDAP + + + + {#if user.ldapServerId} + + + Unlink from LDAP + + {:else} + + + Auto-link to LDAP + + {/if} + + + {/if} +
-

User roles

-
- {#each allRoles as role (role.id)} - {@const activeAssignment = userRoles.find(ur => ur.id === role.id && ur.isActive)} - {@const expiredAssignment = userRoles.find(ur => ur.id === role.id && ur.isExpired)} - {@const isActive = !!activeAssignment} - {@const isExpired = !!expiredAssignment && !isActive} - {@const assignment = activeAssignment ?? expiredAssignment} - {@const status = assignment ? getExpiryStatus(assignment) : null} -
-
- toggleRole(role)} - checked={isActive} /> -
-
{role.name}
- {#if isActive && activeAssignment} -
- - {status?.text ?? ''} - - {#if activeAssignment.grantedAt} - - • - - {/if} -
- {:else if isExpired && expiredAssignment} -
- - Expired - - {#if expiredAssignment.expiresAt} - - - + + + +
+ + {#if $adminPermissions.usersEdit} +
+ +
+ {/if} + +
+
+ {#each allRoles as role (role.id)} + {@const activeAssignment = userRoles.find(ur => ur.id === role.id && ur.isActive)} + {@const expiredAssignment = userRoles.find(ur => ur.id === role.id && ur.isExpired)} + {@const isActive = !!activeAssignment} + {@const isExpired = !!expiredAssignment && !isActive} + {@const assignment = activeAssignment ?? expiredAssignment} + {@const status = assignment ? getExpiryStatus(assignment) : null} +
+
+ toggleRole(role)} + checked={isActive} /> +
+
{role.name}
+ {#if isActive && activeAssignment} +
+ + {status?.text ?? ''} + + {#if activeAssignment.grantedAt} + + • + + {/if} +
+ {:else if isExpired && expiredAssignment} +
+ + Expired + + {#if expiredAssignment.expiresAt} + + + + {/if} +
+ {:else if role.description} + {role.description} {/if}
- {:else if role.description} - {role.description} - {/if} +
+
+ {#if isActive && activeAssignment} + + + Options + + {/if} +
-
-
- {#if isActive && activeAssignment} - - - Options - - {/if} -
+ {/each}
- {/each} -
- -

Admin roles

-
- {#each allAdminRoles as role (role.id)} - - {/each} -
- -

Traffic

- - - - - -

Access restrictions

-
- -
+ + +
+
+ {#each allAdminRoles as role (role.id)} + + {/each} +
+
+ +
+ + + + + + +
+ + + + {#snippet start()} + + Audit log + + {/snippet} + + Update + + Remove + {/if} +
- {#if error} - {error} - {/if} - -
- - Audit log - - - Update + showExpiryModal = false}> diff --git a/warpgate-web/src/admin/index.html b/warpgate-web/src/admin/index.html index 8d9150970..40da7f2d5 100644 --- a/warpgate-web/src/admin/index.html +++ b/warpgate-web/src/admin/index.html @@ -3,7 +3,7 @@ - + Warpgate diff --git a/warpgate-web/src/admin/lib/Section.svelte b/warpgate-web/src/admin/lib/Section.svelte new file mode 100644 index 000000000..0e6cb7fe8 --- /dev/null +++ b/warpgate-web/src/admin/lib/Section.svelte @@ -0,0 +1,40 @@ + + +
+ {#if !props.hideHeading} +

{props.bodyTitle ?? props.title}

+ {/if} + {@render props.children?.()} +
+ + diff --git a/warpgate-web/src/admin/lib/SectionedForm.svelte b/warpgate-web/src/admin/lib/SectionedForm.svelte new file mode 100644 index 000000000..66a428f53 --- /dev/null +++ b/warpgate-web/src/admin/lib/SectionedForm.svelte @@ -0,0 +1,183 @@ + + +
+ {#if sections.length > 0} + + {/if} + + +
+ {@render props.children?.()} +
+
+ + diff --git a/warpgate-web/src/common/PageSummaryBar.svelte b/warpgate-web/src/common/PageSummaryBar.svelte new file mode 100644 index 000000000..4d621966b --- /dev/null +++ b/warpgate-web/src/common/PageSummaryBar.svelte @@ -0,0 +1,32 @@ + + +
+
+

{props.title}

+ {#if props.subtitle} +
{props.subtitle}
+ {/if} +
+ {#if props.extra} +
+ {@render props.extra()} +
+ {/if} +
+ + diff --git a/warpgate-web/src/common/StickyActionBar.svelte b/warpgate-web/src/common/StickyActionBar.svelte new file mode 100644 index 000000000..e940854c2 --- /dev/null +++ b/warpgate-web/src/common/StickyActionBar.svelte @@ -0,0 +1,50 @@ + + +
+
+ {@render props.start?.()} +
+
+ {@render props.children?.()} +
+
+ {@render props.end?.()} +
+
+ + diff --git a/warpgate-web/src/gateway/index.html b/warpgate-web/src/gateway/index.html index 38a3c92a0..571752319 100644 --- a/warpgate-web/src/gateway/index.html +++ b/warpgate-web/src/gateway/index.html @@ -4,7 +4,7 @@ - + Warpgate diff --git a/warpgate-web/src/theme/_theme.scss b/warpgate-web/src/theme/_theme.scss index 1f28f56f0..31edcadc0 100644 --- a/warpgate-web/src/theme/_theme.scss +++ b/warpgate-web/src/theme/_theme.scss @@ -45,7 +45,7 @@ @import "bootstrap/scss/transitions"; @import "bootstrap/scss/dropdown"; @import "bootstrap/scss/button-group"; -// @import "bootstrap/scss/nav"; +@import "bootstrap/scss/nav"; // @import "bootstrap/scss/navbar"; @import "bootstrap/scss/card"; // @import "bootstrap/scss/accordion"; @@ -282,3 +282,23 @@ ul.pagination { align-items: center; gap: 0.5rem; } + +.nav-pills { + background: var(--bs-body-bg); + + .nav-link { + text-decoration: underline; + text-decoration-color: var(--#{$prefix}secondary-color); + + background: var(--#{$prefix}btn-bg); + color: var(--#{$prefix}btn-color); + + + &:active, &.active { + background: var(--bs-list-group-action-active-bg); + .title { + color: var(--bs-list-group-action-active-color); + } + } + } +} diff --git a/warpgate-web/src/theme/vars.dark.scss b/warpgate-web/src/theme/vars.dark.scss index 83a1060dd..cf81d237b 100644 --- a/warpgate-web/src/theme/vars.dark.scss +++ b/warpgate-web/src/theme/vars.dark.scss @@ -98,3 +98,8 @@ $success-text-emphasis: tint-color($success, 60%); $info-text-emphasis: tint-color($info, 60%); $warning-text-emphasis: tint-color($warning, 60%); $danger-text-emphasis: tint-color($danger, 60%); + + +$nav-link-color: $body-color; +$nav-pills-link-active-color: $body-color; +$nav-pills-link-active-bg: $component-active-bg; From 90d2a834016cce4d5314d6a69f9cd3a3e3d95255 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Mon, 25 May 2026 10:11:29 +0200 Subject: [PATCH 098/556] Bump cryptography from 46.0.7 to 48.0.0 in /tests (#1927) Signed-off-by: dependabot[bot] --- tests/poetry.lock | 115 ++++++++++++++++++++----------------------- tests/pyproject.toml | 2 +- 2 files changed, 55 insertions(+), 62 deletions(-) diff --git a/tests/poetry.lock b/tests/poetry.lock index 1aa3a911d..bef88ad53 100644 --- a/tests/poetry.lock +++ b/tests/poetry.lock @@ -1,4 +1,4 @@ -# This file is automatically @generated by Poetry 2.2.1 and should not be changed by hand. +# This file is automatically @generated by Poetry 2.4.1 and should not be changed by hand. [[package]] name = "aiohappyeyeballs" @@ -629,76 +629,69 @@ files = [ [[package]] name = "cryptography" -version = "46.0.7" +version = "48.0.0" description = "cryptography is a package which provides cryptographic recipes and primitives to Python developers." optional = false -python-versions = "!=3.9.0,!=3.9.1,>=3.8" +python-versions = "!=3.9.0,!=3.9.1,>=3.9" groups = ["main"] files = [ - {file = "cryptography-46.0.7-cp311-abi3-macosx_10_9_universal2.whl", hash = "sha256:ea42cbe97209df307fdc3b155f1b6fa2577c0defa8f1f7d3be7d31d189108ad4"}, - {file = "cryptography-46.0.7-cp311-abi3-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:b36a4695e29fe69215d75960b22577197aca3f7a25b9cf9d165dcfe9d80bc325"}, - {file = "cryptography-46.0.7-cp311-abi3-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:5ad9ef796328c5e3c4ceed237a183f5d41d21150f972455a9d926593a1dcb308"}, - {file = "cryptography-46.0.7-cp311-abi3-manylinux_2_28_aarch64.whl", hash = "sha256:73510b83623e080a2c35c62c15298096e2a5dc8d51c3b4e1740211839d0dea77"}, - {file = "cryptography-46.0.7-cp311-abi3-manylinux_2_28_ppc64le.whl", hash = "sha256:cbd5fb06b62bd0721e1170273d3f4d5a277044c47ca27ee257025146c34cbdd1"}, - {file = "cryptography-46.0.7-cp311-abi3-manylinux_2_28_x86_64.whl", hash = "sha256:420b1e4109cc95f0e5700eed79908cef9268265c773d3a66f7af1eef53d409ef"}, - {file = "cryptography-46.0.7-cp311-abi3-manylinux_2_31_armv7l.whl", hash = "sha256:24402210aa54baae71d99441d15bb5a1919c195398a87b563df84468160a65de"}, - {file = "cryptography-46.0.7-cp311-abi3-manylinux_2_34_aarch64.whl", hash = "sha256:8a469028a86f12eb7d2fe97162d0634026d92a21f3ae0ac87ed1c4a447886c83"}, - {file = "cryptography-46.0.7-cp311-abi3-manylinux_2_34_ppc64le.whl", hash = "sha256:9694078c5d44c157ef3162e3bf3946510b857df5a3955458381d1c7cfc143ddb"}, - {file = "cryptography-46.0.7-cp311-abi3-manylinux_2_34_x86_64.whl", hash = "sha256:42a1e5f98abb6391717978baf9f90dc28a743b7d9be7f0751a6f56a75d14065b"}, - {file = "cryptography-46.0.7-cp311-abi3-musllinux_1_2_aarch64.whl", hash = "sha256:91bbcb08347344f810cbe49065914fe048949648f6bd5c2519f34619142bbe85"}, - {file = "cryptography-46.0.7-cp311-abi3-musllinux_1_2_x86_64.whl", hash = "sha256:5d1c02a14ceb9148cc7816249f64f623fbfee39e8c03b3650d842ad3f34d637e"}, - {file = "cryptography-46.0.7-cp311-abi3-win32.whl", hash = "sha256:d23c8ca48e44ee015cd0a54aeccdf9f09004eba9fc96f38c911011d9ff1bd457"}, - {file = "cryptography-46.0.7-cp311-abi3-win_amd64.whl", hash = "sha256:397655da831414d165029da9bc483bed2fe0e75dde6a1523ec2fe63f3c46046b"}, - {file = "cryptography-46.0.7-cp314-cp314t-macosx_10_9_universal2.whl", hash = "sha256:d151173275e1728cf7839aaa80c34fe550c04ddb27b34f48c232193df8db5842"}, - {file = "cryptography-46.0.7-cp314-cp314t-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:db0f493b9181c7820c8134437eb8b0b4792085d37dbb24da050476ccb664e59c"}, - {file = "cryptography-46.0.7-cp314-cp314t-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:ebd6daf519b9f189f85c479427bbd6e9c9037862cf8fe89ee35503bd209ed902"}, - {file = "cryptography-46.0.7-cp314-cp314t-manylinux_2_28_aarch64.whl", hash = "sha256:b7b412817be92117ec5ed95f880defe9cf18a832e8cafacf0a22337dc1981b4d"}, - {file = "cryptography-46.0.7-cp314-cp314t-manylinux_2_28_ppc64le.whl", hash = "sha256:fbfd0e5f273877695cb93baf14b185f4878128b250cc9f8e617ea0c025dfb022"}, - {file = "cryptography-46.0.7-cp314-cp314t-manylinux_2_28_x86_64.whl", hash = "sha256:ffca7aa1d00cf7d6469b988c581598f2259e46215e0140af408966a24cf086ce"}, - {file = "cryptography-46.0.7-cp314-cp314t-manylinux_2_31_armv7l.whl", hash = "sha256:60627cf07e0d9274338521205899337c5d18249db56865f943cbe753aa96f40f"}, - {file = "cryptography-46.0.7-cp314-cp314t-manylinux_2_34_aarch64.whl", hash = "sha256:80406c3065e2c55d7f49a9550fe0c49b3f12e5bfff5dedb727e319e1afb9bf99"}, - {file = "cryptography-46.0.7-cp314-cp314t-manylinux_2_34_ppc64le.whl", hash = "sha256:c5b1ccd1239f48b7151a65bc6dd54bcfcc15e028c8ac126d3fada09db0e07ef1"}, - {file = "cryptography-46.0.7-cp314-cp314t-manylinux_2_34_x86_64.whl", hash = "sha256:d5f7520159cd9c2154eb61eb67548ca05c5774d39e9c2c4339fd793fe7d097b2"}, - {file = "cryptography-46.0.7-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:fcd8eac50d9138c1d7fc53a653ba60a2bee81a505f9f8850b6b2888555a45d0e"}, - {file = "cryptography-46.0.7-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:65814c60f8cc400c63131584e3e1fad01235edba2614b61fbfbfa954082db0ee"}, - {file = "cryptography-46.0.7-cp314-cp314t-win32.whl", hash = "sha256:fdd1736fed309b4300346f88f74cd120c27c56852c3838cab416e7a166f67298"}, - {file = "cryptography-46.0.7-cp314-cp314t-win_amd64.whl", hash = "sha256:e06acf3c99be55aa3b516397fe42f5855597f430add9c17fa46bf2e0fb34c9bb"}, - {file = "cryptography-46.0.7-cp38-abi3-macosx_10_9_universal2.whl", hash = "sha256:462ad5cb1c148a22b2e3bcc5ad52504dff325d17daf5df8d88c17dda1f75f2a4"}, - {file = "cryptography-46.0.7-cp38-abi3-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:84d4cced91f0f159a7ddacad249cc077e63195c36aac40b4150e7a57e84fffe7"}, - {file = "cryptography-46.0.7-cp38-abi3-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:128c5edfe5e5938b86b03941e94fac9ee793a94452ad1365c9fc3f4f62216832"}, - {file = "cryptography-46.0.7-cp38-abi3-manylinux_2_28_aarch64.whl", hash = "sha256:5e51be372b26ef4ba3de3c167cd3d1022934bc838ae9eaad7e644986d2a3d163"}, - {file = "cryptography-46.0.7-cp38-abi3-manylinux_2_28_ppc64le.whl", hash = "sha256:cdf1a610ef82abb396451862739e3fc93b071c844399e15b90726ef7470eeaf2"}, - {file = "cryptography-46.0.7-cp38-abi3-manylinux_2_28_x86_64.whl", hash = "sha256:1d25aee46d0c6f1a501adcddb2d2fee4b979381346a78558ed13e50aa8a59067"}, - {file = "cryptography-46.0.7-cp38-abi3-manylinux_2_31_armv7l.whl", hash = "sha256:cdfbe22376065ffcf8be74dc9a909f032df19bc58a699456a21712d6e5eabfd0"}, - {file = "cryptography-46.0.7-cp38-abi3-manylinux_2_34_aarch64.whl", hash = "sha256:abad9dac36cbf55de6eb49badd4016806b3165d396f64925bf2999bcb67837ba"}, - {file = "cryptography-46.0.7-cp38-abi3-manylinux_2_34_ppc64le.whl", hash = "sha256:935ce7e3cfdb53e3536119a542b839bb94ec1ad081013e9ab9b7cfd478b05006"}, - {file = "cryptography-46.0.7-cp38-abi3-manylinux_2_34_x86_64.whl", hash = "sha256:35719dc79d4730d30f1c2b6474bd6acda36ae2dfae1e3c16f2051f215df33ce0"}, - {file = "cryptography-46.0.7-cp38-abi3-musllinux_1_2_aarch64.whl", hash = "sha256:7bbc6ccf49d05ac8f7d7b5e2e2c33830d4fe2061def88210a126d130d7f71a85"}, - {file = "cryptography-46.0.7-cp38-abi3-musllinux_1_2_x86_64.whl", hash = "sha256:a1529d614f44b863a7b480c6d000fe93b59acee9c82ffa027cfadc77521a9f5e"}, - {file = "cryptography-46.0.7-cp38-abi3-win32.whl", hash = "sha256:f247c8c1a1fb45e12586afbb436ef21ff1e80670b2861a90353d9b025583d246"}, - {file = "cryptography-46.0.7-cp38-abi3-win_amd64.whl", hash = "sha256:506c4ff91eff4f82bdac7633318a526b1d1309fc07ca76a3ad182cb5b686d6d3"}, - {file = "cryptography-46.0.7-pp311-pypy311_pp73-macosx_11_0_arm64.whl", hash = "sha256:fc9ab8856ae6cf7c9358430e49b368f3108f050031442eaeb6b9d87e4dcf4e4f"}, - {file = "cryptography-46.0.7-pp311-pypy311_pp73-manylinux_2_28_aarch64.whl", hash = "sha256:d3b99c535a9de0adced13d159c5a9cf65c325601aa30f4be08afd680643e9c15"}, - {file = "cryptography-46.0.7-pp311-pypy311_pp73-manylinux_2_28_x86_64.whl", hash = "sha256:d02c738dacda7dc2a74d1b2b3177042009d5cab7c7079db74afc19e56ca1b455"}, - {file = "cryptography-46.0.7-pp311-pypy311_pp73-manylinux_2_34_aarch64.whl", hash = "sha256:04959522f938493042d595a736e7dbdff6eb6cc2339c11465b3ff89343b65f65"}, - {file = "cryptography-46.0.7-pp311-pypy311_pp73-manylinux_2_34_x86_64.whl", hash = "sha256:3986ac1dee6def53797289999eabe84798ad7817f3e97779b5061a95b0ee4968"}, - {file = "cryptography-46.0.7-pp311-pypy311_pp73-win_amd64.whl", hash = "sha256:258514877e15963bd43b558917bc9f54cf7cf866c38aa576ebf47a77ddbc43a4"}, - {file = "cryptography-46.0.7.tar.gz", hash = "sha256:e4cfd68c5f3e0bfdad0d38e023239b96a2fe84146481852dffbcca442c245aa5"}, + {file = "cryptography-48.0.0-cp311-abi3-macosx_10_9_universal2.whl", hash = "sha256:0c558d2cdffd8f4bbb30fc7134c74d2ca9a476f830bb053074498fbc86f41ed6"}, + {file = "cryptography-48.0.0-cp311-abi3-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:f5333311663ea94f75dd408665686aaf426563556bb5283554a3539177e03b8c"}, + {file = "cryptography-48.0.0-cp311-abi3-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:7995ef305d7165c3f11ae07f2517e5a4f1d5c18da1376a0a9ed496336b69e5f3"}, + {file = "cryptography-48.0.0-cp311-abi3-manylinux_2_28_aarch64.whl", hash = "sha256:40ba1f85eaa6959837b1d51c9767e230e14612eea4ef110ee8854ada22da1bf5"}, + {file = "cryptography-48.0.0-cp311-abi3-manylinux_2_28_ppc64le.whl", hash = "sha256:369a6348999f94bbd53435c894377b20ab95f25a9065c283570e70150d8abc3c"}, + {file = "cryptography-48.0.0-cp311-abi3-manylinux_2_28_x86_64.whl", hash = "sha256:a0e692c683f4df67815a2d258b324e66f4738bd7a96a218c826dce4f4bd05d8f"}, + {file = "cryptography-48.0.0-cp311-abi3-manylinux_2_31_armv7l.whl", hash = "sha256:18349bbc56f4743c8b12dc32e2bccb2cf83ee8b69a3bba74ef8ae857e26b3d25"}, + {file = "cryptography-48.0.0-cp311-abi3-manylinux_2_34_aarch64.whl", hash = "sha256:7e8eac43dfca5c4cccc6dad9a80504436fca53bb9bc3100a2386d730fbe6b602"}, + {file = "cryptography-48.0.0-cp311-abi3-manylinux_2_34_ppc64le.whl", hash = "sha256:9ccdac7d40688ecb5a3b4a604b8a88c8002e3442d6c60aead1db2a89a041560c"}, + {file = "cryptography-48.0.0-cp311-abi3-manylinux_2_34_x86_64.whl", hash = "sha256:bd72e68b06bb1e96913f97dd4901119bc17f39d4586a5adf2d3e47bc2b9d58b5"}, + {file = "cryptography-48.0.0-cp311-abi3-musllinux_1_2_aarch64.whl", hash = "sha256:59baa2cb386c4f0b9905bd6eb4c2a79a69a128408fd31d32ca4d7102d4156321"}, + {file = "cryptography-48.0.0-cp311-abi3-musllinux_1_2_x86_64.whl", hash = "sha256:9249e3cd978541d665967ac2cb2787fd6a62bddf1e75b3e347a594d7dacf4f74"}, + {file = "cryptography-48.0.0-cp311-abi3-win32.whl", hash = "sha256:9c459db21422be75e2809370b829a87eb37f74cd785fc4aa9ea1e5f43b47cda4"}, + {file = "cryptography-48.0.0-cp311-abi3-win_amd64.whl", hash = "sha256:5b012212e08b8dd5edc78ef54da83dd9892fd9105323b3993eff6bea65dc21d7"}, + {file = "cryptography-48.0.0-cp314-cp314t-macosx_10_9_universal2.whl", hash = "sha256:3cb07a3ed6431663cd321ea8a000a1314c74211f823e4177fefa2255e057d1ec"}, + {file = "cryptography-48.0.0-cp314-cp314t-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:8c7378637d7d88016fa6791c159f698b3d3eed28ebf844ac36b9dc04a14dae18"}, + {file = "cryptography-48.0.0-cp314-cp314t-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:cc90c0b39b2e3c65ef52c804b72e3c58f8a04ab2a1871272798e5f9572c17d20"}, + {file = "cryptography-48.0.0-cp314-cp314t-manylinux_2_28_aarch64.whl", hash = "sha256:76341972e1eff8b4bea859f09c0d3e64b96ce931b084f9b9b7db8ef364c30eff"}, + {file = "cryptography-48.0.0-cp314-cp314t-manylinux_2_28_ppc64le.whl", hash = "sha256:55b7718303bf06a5753dcdccf2f3945cf18ad7bffde41b61226e4db31ab89a9c"}, + {file = "cryptography-48.0.0-cp314-cp314t-manylinux_2_28_x86_64.whl", hash = "sha256:a64697c641c7b1b2178e573cbc31c7c6684cd56883a478d75143dbb7118036db"}, + {file = "cryptography-48.0.0-cp314-cp314t-manylinux_2_31_armv7l.whl", hash = "sha256:561215ea3879cb1cbbf272867e2efda62476f240fb58c64de6b393ae19246741"}, + {file = "cryptography-48.0.0-cp314-cp314t-manylinux_2_34_aarch64.whl", hash = "sha256:ad64688338ed4bc1a6618076ba75fd7194a5f1797ac60b47afe926285adb3166"}, + {file = "cryptography-48.0.0-cp314-cp314t-manylinux_2_34_ppc64le.whl", hash = "sha256:906cbf0670286c6e0044156bc7d4af9cbb0ef6db9f73e52c3ec56ba6bdde5336"}, + {file = "cryptography-48.0.0-cp314-cp314t-manylinux_2_34_x86_64.whl", hash = "sha256:ea8990436d914540a40ab24b6a77c0969695ed52f4a4874c5137ccf7045a7057"}, + {file = "cryptography-48.0.0-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:c18684a7f0cc9a3cb60328f496b8e3372def7c5d2df39ac267878b05565aaaae"}, + {file = "cryptography-48.0.0-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:9be5aafa5736574f8f15f262adc81b2a9869e2cfe9014d52a44633905b40d52c"}, + {file = "cryptography-48.0.0-cp314-cp314t-win32.whl", hash = "sha256:c17dfe85494deaeddc5ce251aebd1d60bbe6afc8b62071bb0b469431a000124f"}, + {file = "cryptography-48.0.0-cp314-cp314t-win_amd64.whl", hash = "sha256:27241b1dc9962e056062a8eef1991d02c3a24569c95975bd2322a8a52c6e5e12"}, + {file = "cryptography-48.0.0-cp39-abi3-macosx_10_9_universal2.whl", hash = "sha256:58d00498e8933e4a194f3076aee1b4a97dfec1a6da444535755822fe5d8b0b86"}, + {file = "cryptography-48.0.0-cp39-abi3-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:614d0949f4790582d2cc25553abd09dd723025f0c0e7c67376a1d77196743d6e"}, + {file = "cryptography-48.0.0-cp39-abi3-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:7ce4bfae76319a532a2dc68f82cc32f5676ee792a983187dac07183690e5c66f"}, + {file = "cryptography-48.0.0-cp39-abi3-manylinux_2_28_aarch64.whl", hash = "sha256:2eb992bbd4661238c5a397594c83f5b4dc2bc5b848c365c8f991b6780efcc5c7"}, + {file = "cryptography-48.0.0-cp39-abi3-manylinux_2_28_ppc64le.whl", hash = "sha256:22a5cb272895dce158b2cacdfdc3debd299019659f42947dbdac6f32d68fe832"}, + {file = "cryptography-48.0.0-cp39-abi3-manylinux_2_28_x86_64.whl", hash = "sha256:2b4d59804e8408e2fea7d1fbaf218e5ec984325221db76e6a241a9abd6cdd95c"}, + {file = "cryptography-48.0.0-cp39-abi3-manylinux_2_31_armv7l.whl", hash = "sha256:984a20b0f62a26f48a3396c72e4bc34c66e356d356bf370053066b3b6d54634a"}, + {file = "cryptography-48.0.0-cp39-abi3-manylinux_2_34_aarch64.whl", hash = "sha256:5a5ed8fde7a1d09376ca0b40e68cd59c69fe23b1f9768bd5824f54681626032a"}, + {file = "cryptography-48.0.0-cp39-abi3-manylinux_2_34_ppc64le.whl", hash = "sha256:8cd666227ef7af430aa5914a9910e0ddd703e75f039cef0825cd0da71b6b711a"}, + {file = "cryptography-48.0.0-cp39-abi3-manylinux_2_34_x86_64.whl", hash = "sha256:9071196d81abc88b3516ac8cdfad32e2b66dd4a5393a8e68a961e9161ddc6239"}, + {file = "cryptography-48.0.0-cp39-abi3-musllinux_1_2_aarch64.whl", hash = "sha256:1e2d54c8be6152856a36f0882ab231e70f8ec7f14e93cf87db8a2ed056bf160c"}, + {file = "cryptography-48.0.0-cp39-abi3-musllinux_1_2_x86_64.whl", hash = "sha256:a5da777e32ffed6f85a7b2b3f7c5cbc88c146bfcd0a1d7baf5fcc6c52ee35dd4"}, + {file = "cryptography-48.0.0-cp39-abi3-win32.whl", hash = "sha256:77a2ccbbe917f6710e05ba9adaa25fb5075620bf3ea6fb751997875aff4ae4bd"}, + {file = "cryptography-48.0.0-cp39-abi3-win_amd64.whl", hash = "sha256:16cd65b9330583e4619939b3a3843eec1e6e789744bb01e7c7e2e62e33c239c8"}, + {file = "cryptography-48.0.0-pp311-pypy311_pp73-macosx_11_0_arm64.whl", hash = "sha256:84cf79f0dc8b36ac5da873481716e87aef31fcfa0444f9e1d8b4b2cece142855"}, + {file = "cryptography-48.0.0-pp311-pypy311_pp73-manylinux_2_28_aarch64.whl", hash = "sha256:fdfef35d751d510fcef5252703621574364fec16418c4a1e5e1055248401054b"}, + {file = "cryptography-48.0.0-pp311-pypy311_pp73-manylinux_2_28_x86_64.whl", hash = "sha256:0890f502ddf7d9c6426129c3f49f5c0a39278ed7cd6322c8755ffca6ee675a13"}, + {file = "cryptography-48.0.0-pp311-pypy311_pp73-manylinux_2_34_aarch64.whl", hash = "sha256:ecde28a596bead48b0cfd2a1b4416c3d43074c2d785e3a398d7ec1fc4d0f7fbb"}, + {file = "cryptography-48.0.0-pp311-pypy311_pp73-manylinux_2_34_x86_64.whl", hash = "sha256:4defde8685ae324a9eb9d818717e93b4638ef67070ac9bc15b8ca85f63048355"}, + {file = "cryptography-48.0.0-pp311-pypy311_pp73-win_amd64.whl", hash = "sha256:db63bf618e5dea46c07de12e900fe1cdd2541e6dc9dbae772a70b7d4d4765f6a"}, + {file = "cryptography-48.0.0.tar.gz", hash = "sha256:5c3932f4436d1cccb036cb0eaef46e6e2db91035166f1ad6505c3c9d5a635920"}, ] [package.dependencies] -cffi = {version = ">=2.0.0", markers = "python_full_version >= \"3.9.0\" and platform_python_implementation != \"PyPy\""} +cffi = {version = ">=2.0.0", markers = "platform_python_implementation != \"PyPy\""} typing-extensions = {version = ">=4.13.2", markers = "python_full_version < \"3.11.0\""} [package.extras] -docs = ["sphinx (>=5.3.0)", "sphinx-inline-tabs", "sphinx-rtd-theme (>=3.0.0)"] -docstest = ["pyenchant (>=3)", "readme-renderer (>=30.0)", "sphinxcontrib-spelling (>=7.3.1)"] -nox = ["nox[uv] (>=2024.4.15)"] -pep8test = ["check-sdist", "click (>=8.0.1)", "mypy (>=1.14)", "ruff (>=0.11.11)"] -sdist = ["build (>=1.0.0)"] ssh = ["bcrypt (>=3.1.5)"] -test = ["certifi (>=2024)", "cryptography-vectors (==46.0.7)", "pretend (>=0.7)", "pytest (>=7.4.0)", "pytest-benchmark (>=4.0)", "pytest-cov (>=2.10.1)", "pytest-xdist (>=3.5.0)"] -test-randomorder = ["pytest-randomly"] [[package]] name = "deepmerge" @@ -2418,4 +2411,4 @@ propcache = ">=0.2.1" [metadata] lock-version = "2.1" python-versions = "^3.10" -content-hash = "5e27d6c4a8c2f847b836e9a42a0865ef550655bef8832efd2b4dc2c63e897692" +content-hash = "fe6e9f9cbe5278175aaf55d2e20e8f373574cddb20293a0ec4d9ab87f94baa05" diff --git a/tests/pyproject.toml b/tests/pyproject.toml index 6505d4bd9..a46a09000 100644 --- a/tests/pyproject.toml +++ b/tests/pyproject.toml @@ -18,7 +18,7 @@ PyYAML = "^6.0.2" deepmerge = "^2" openapi-client = { path = "./api_sdk", develop = true } aiohttp = "^3.11.18" -cryptography = "^46" +cryptography = ">=46,<49" [tool.poetry.dev-dependencies] flake8 = "^7.3.0" From 1f76153c7b95f209a9c9a9ec3d7d990b01912d2d Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Mon, 25 May 2026 10:11:36 +0200 Subject: [PATCH 099/556] Bump github/codeql-action from 4.35.5 to 4.36.0 (#1969) Signed-off-by: dependabot[bot] --- .github/workflows/codeql.yml | 4 ++-- .github/workflows/scorecard.yml | 2 +- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/.github/workflows/codeql.yml b/.github/workflows/codeql.yml index e78fe1a6c..9c2beedeb 100644 --- a/.github/workflows/codeql.yml +++ b/.github/workflows/codeql.yml @@ -62,7 +62,7 @@ jobs: # Initializes the CodeQL tools for scanning. - name: Initialize CodeQL - uses: github/codeql-action/init@9e0d7b8d25671d64c341c19c0152d693099fb5ba + uses: github/codeql-action/init@7211b7c8077ea37d8641b6271f6a365a22a5fbfa with: languages: ${{ matrix.language }} build-mode: ${{ matrix.build-mode }} @@ -74,6 +74,6 @@ jobs: # queries: security-extended,security-and-quality - name: Perform CodeQL Analysis - uses: github/codeql-action/analyze@9e0d7b8d25671d64c341c19c0152d693099fb5ba + uses: github/codeql-action/analyze@7211b7c8077ea37d8641b6271f6a365a22a5fbfa with: category: "/language:${{matrix.language}}" diff --git a/.github/workflows/scorecard.yml b/.github/workflows/scorecard.yml index 330596f79..cb22fe7b6 100644 --- a/.github/workflows/scorecard.yml +++ b/.github/workflows/scorecard.yml @@ -73,6 +73,6 @@ jobs: # Upload the results to GitHub's code scanning dashboard (optional). # Commenting out will disable upload of results to your repo's Code Scanning dashboard - name: "Upload to code-scanning" - uses: github/codeql-action/upload-sarif@9e0d7b8d25671d64c341c19c0152d693099fb5ba # v4.35.5 + uses: github/codeql-action/upload-sarif@7211b7c8077ea37d8641b6271f6a365a22a5fbfa # v4.36.0 with: sarif_file: results.sarif From e9d91631b3ae6aaa4b5b95bb932fb0a232285e4f Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Mon, 25 May 2026 10:11:46 +0200 Subject: [PATCH 100/556] Bump docker/login-action from 4.1.0 to 4.2.0 (#1968) Signed-off-by: dependabot[bot] --- .github/workflows/docker.yml | 4 ++-- .github/workflows/helm-publish.yaml | 2 +- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/.github/workflows/docker.yml b/.github/workflows/docker.yml index 7b176e0f9..7f79a0fd3 100644 --- a/.github/workflows/docker.yml +++ b/.github/workflows/docker.yml @@ -47,7 +47,7 @@ jobs: - name: Log into registry ${{ env.REGISTRY }} if: github.event_name != 'pull_request' || github.event.pull_request.head.repo.full_name == github.repository - uses: docker/login-action@4907a6ddec9925e35a0a9e82d7399ccc52663121 + uses: docker/login-action@650006c6eb7dba73a995cc03b0b2d7f5ca915bee with: registry: ${{ env.REGISTRY }} username: ${{ github.actor }} @@ -127,7 +127,7 @@ jobs: uses: docker/setup-buildx-action@4d04d5d9486b7bd6fa91e7baf45bbb4f8b9deedd - name: Log into registry ${{ env.REGISTRY }} - uses: docker/login-action@4907a6ddec9925e35a0a9e82d7399ccc52663121 + uses: docker/login-action@650006c6eb7dba73a995cc03b0b2d7f5ca915bee with: registry: ${{ env.REGISTRY }} username: ${{ github.actor }} diff --git a/.github/workflows/helm-publish.yaml b/.github/workflows/helm-publish.yaml index 60add70a3..6d2a942c8 100644 --- a/.github/workflows/helm-publish.yaml +++ b/.github/workflows/helm-publish.yaml @@ -62,7 +62,7 @@ jobs: cat ${{ env.CHART_PATH }}/Chart.yaml - name: Log into registry ${{ env.REGISTRY }} - uses: docker/login-action@4907a6ddec9925e35a0a9e82d7399ccc52663121 + uses: docker/login-action@650006c6eb7dba73a995cc03b0b2d7f5ca915bee with: registry: ${{ env.REGISTRY }} username: ${{ github.actor }} From c4cd8e839aaab5e55ee95e6fc89aef6a2ed97f35 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Mon, 25 May 2026 10:11:54 +0200 Subject: [PATCH 101/556] Bump docker/metadata-action from 6.0.0 to 6.1.0 (#1967) Signed-off-by: dependabot[bot] --- .github/workflows/docker.yml | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/.github/workflows/docker.yml b/.github/workflows/docker.yml index 7f79a0fd3..84e231ff3 100644 --- a/.github/workflows/docker.yml +++ b/.github/workflows/docker.yml @@ -55,7 +55,7 @@ jobs: - name: Docker meta id: meta - uses: docker/metadata-action@030e881283bb7a6894de51c315a6bfe6a94e05cf + uses: docker/metadata-action@80c7e94dd9b9319bd5eb7a0e0fe9291e23a2a2e9 with: images: ${{ env.REGISTRY }}/${{ env.IMAGE_NAME }} @@ -135,7 +135,7 @@ jobs: - name: Docker meta id: meta - uses: docker/metadata-action@030e881283bb7a6894de51c315a6bfe6a94e05cf + uses: docker/metadata-action@80c7e94dd9b9319bd5eb7a0e0fe9291e23a2a2e9 with: images: ${{ env.REGISTRY }}/${{ env.IMAGE_NAME }} tags: | From 98af0cbd5474d16fee71050f9aa4d5ad530af36f Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Mon, 25 May 2026 10:12:02 +0200 Subject: [PATCH 102/556] Bump russh from 0.60.2 to 0.60.3 (#1965) Signed-off-by: dependabot[bot] --- Cargo.lock | 8 ++++---- Cargo.toml | 2 +- 2 files changed, 5 insertions(+), 5 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index e875b7be5..bc3d48500 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -5088,9 +5088,9 @@ dependencies = [ [[package]] name = "russh" -version = "0.60.2" +version = "0.60.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9c9e358980fe9b079b99da387117864ee6f0a3fd02f39e5b5fde6af9c2895374" +checksum = "324b92f459d3e42da294e14e8eb150d2215fcfb7c966838bc1127cd68bc05a0d" dependencies = [ "aead 0.6.0-rc.10", "aes 0.8.4", @@ -5170,9 +5170,9 @@ dependencies = [ [[package]] name = "russh-cryptovec" -version = "0.59.0" +version = "0.60.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "36140e8a20297bc2e8338807c3d9ca911f7fa49d7539cbcd6d48d3befd70efd8" +checksum = "37cb4d0360bdd8935392a306d8b5edb539cc455b30e8bf13dd213a0cf7879b40" dependencies = [ "log", "nix 0.31.2", diff --git a/Cargo.toml b/Cargo.toml index bfc266576..7507cced7 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -32,7 +32,7 @@ data-encoding = { version = "2.3", default-features = false, features = ["alloc" ipnet = "2" serde = { version = "1.0", features = ["derive"], default-features = false } serde_json = { version = "1.0", default-features = false } -russh = { version = "0.60.1", features = ["des", "rsa", "aws-lc-rs"], default-features = false } +russh = { version = "0.60.3", features = ["des", "rsa", "aws-lc-rs"], default-features = false } futures = { version = "0.3", default-features = false } tokio-stream = { version = "0.1.17", features = ["net"], default-features = false } tokio-rustls = { version = "0.26", default-features = false } From af7695879a65f3ad9c4c3aeb6a187f8fa247ff13 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Mon, 25 May 2026 10:12:10 +0200 Subject: [PATCH 103/556] Bump docker/build-push-action from 7.1.0 to 7.2.0 (#1964) Signed-off-by: dependabot[bot] --- .github/workflows/docker.yml | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/.github/workflows/docker.yml b/.github/workflows/docker.yml index 84e231ff3..98cca4eb3 100644 --- a/.github/workflows/docker.yml +++ b/.github/workflows/docker.yml @@ -62,7 +62,7 @@ jobs: - name: Build Docker image without pushing if: github.event_name == 'pull_request' && github.event.pull_request.head.repo.full_name != github.repository id: build-no-push - uses: docker/build-push-action@bcafcacb16a39f128d818304e6c9c0c18556b85f + uses: docker/build-push-action@f9f3042f7e2789586610d6e8b85c8f03e5195baf with: file: docker/Dockerfile context: . @@ -74,7 +74,7 @@ jobs: - name: Build and push Docker image if: github.event_name != 'pull_request' || github.event.pull_request.head.repo.full_name == github.repository id: build - uses: docker/build-push-action@bcafcacb16a39f128d818304e6c9c0c18556b85f + uses: docker/build-push-action@f9f3042f7e2789586610d6e8b85c8f03e5195baf with: file: docker/Dockerfile context: . From 844c0e53a2ce8d19bc79a1a846d989f059333a3e Mon Sep 17 00:00:00 2001 From: "allcontributors[bot]" <46447321+allcontributors[bot]@users.noreply.github.com> Date: Mon, 25 May 2026 12:37:05 +0200 Subject: [PATCH 104/556] add xTamasu as a contributor for code (#1972) --- .all-contributorsrc | 9 +++++++++ README.md | 1 + 2 files changed, 10 insertions(+) diff --git a/.all-contributorsrc b/.all-contributorsrc index a4de94a91..ba16f6c4c 100644 --- a/.all-contributorsrc +++ b/.all-contributorsrc @@ -180,6 +180,15 @@ "contributions": [ "code" ] + }, + { + "login": "xTamasu", + "name": "Lukas Klepper", + "avatar_url": "https://avatars.githubusercontent.com/u/20605096?v=4", + "profile": "https://github.com/xTamasu", + "contributions": [ + "code" + ] } ], "contributorsPerLine": 7, diff --git a/README.md b/README.md index 4ee00ba00..2dcb44c25 100644 --- a/README.md +++ b/README.md @@ -158,6 +158,7 @@ Thanks goes to these wonderful people ([emoji key](https://allcontributors.org/d Sambhavi Pandey
Sambhavi Pandey

💻 Tina
Tina

💻 Immanuel Tikhonov
Immanuel Tikhonov

💻 + Lukas Klepper
Lukas Klepper

💻 From 2d9e6c52275d5da6f9b0367384df2b778891103c Mon Sep 17 00:00:00 2001 From: Lukas Klepper Date: Mon, 25 May 2026 21:44:25 +0200 Subject: [PATCH 105/556] fix: display security key and browser auth URL in SSH terminal (#1960) (#1970) Co-authored-by: Eugene --- Cargo.lock | 1 + tests/conftest.py | 2 +- tests/test_ssh_user_auth_otp.py | 1 - tests/test_ssh_user_auth_otp_and_web.py | 195 ++++++++++++++++++ warpgate-protocol-ssh/Cargo.toml | 1 + .../src/server/russh_handler.rs | 16 +- warpgate-protocol-ssh/src/server/session.rs | 175 +++++++++------- 7 files changed, 301 insertions(+), 90 deletions(-) create mode 100644 tests/test_ssh_user_auth_otp_and_web.py diff --git a/Cargo.lock b/Cargo.lock index bc3d48500..24c812ad2 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -7575,6 +7575,7 @@ dependencies = [ "time", "tokio", "tracing", + "url", "uuid", "warpgate-aws", "warpgate-common", diff --git a/tests/conftest.py b/tests/conftest.py index f3f7085fc..b74b8baf0 100644 --- a/tests/conftest.py +++ b/tests/conftest.py @@ -797,7 +797,7 @@ def shared_wg(processes: ProcessManager): # endpoint. previously everyone called ``admin_client(url)`` directly; # a fixture lets us compute the URL from ``shared_wg`` once and removes # boilerplate from individual tests. -from .api_client import admin_client as _admin_client_context +from .api_client import admin_client as _admin_client_context # noqa: E402 @pytest.fixture diff --git a/tests/test_ssh_user_auth_otp.py b/tests/test_ssh_user_auth_otp.py index e4a24bba5..6b12095f2 100644 --- a/tests/test_ssh_user_auth_otp.py +++ b/tests/test_ssh_user_auth_otp.py @@ -2,7 +2,6 @@ from base64 import b64decode from uuid import uuid4 import pyotp -import pytest from pathlib import Path from textwrap import dedent diff --git a/tests/test_ssh_user_auth_otp_and_web.py b/tests/test_ssh_user_auth_otp_and_web.py new file mode 100644 index 000000000..66a724c67 --- /dev/null +++ b/tests/test_ssh_user_auth_otp_and_web.py @@ -0,0 +1,195 @@ +import asyncio +import subprocess +import tempfile +from base64 import b64decode +from pathlib import Path +from textwrap import dedent +from uuid import uuid4 + +import aiohttp +import pyotp +import pytest + +from .api_client import admin_client, sdk +from .conftest import ProcessManager, WarpgateProcess +from .util import wait_port + + +class Test: + @pytest.mark.asyncio + async def test_otp_and_web_auth( + self, + processes: ProcessManager, + wg_c_ed25519_pubkey: Path, + otp_key_base32: str, + otp_key_base64: str, + timeout, + shared_wg: WarpgateProcess, + ): + ssh_port = processes.start_ssh_server( + trusted_keys=[wg_c_ed25519_pubkey.read_text()] + ) + wait_port(ssh_port) + + url = f"https://localhost:{shared_wg.http_port}" + with admin_client(url) as api: + role = api.create_role(sdk.RoleDataRequest(name=f"role-{uuid4()}")) + user = api.create_user(sdk.CreateUserRequest(username=f"user-{uuid4()}")) + api.create_public_key_credential( + user.id, + sdk.NewPublicKeyCredential( + label="Public Key", + openssh_public_key=open("ssh-keys/id_ed25519.pub").read().strip(), + ), + ) + api.create_otp_credential( + user.id, + sdk.NewOtpCredential(secret_key=list(b64decode(otp_key_base64))), + ) + api.create_password_credential( + user.id, sdk.NewPasswordCredential(password="123") + ) + api.update_user( + user.id, + sdk.UserDataRequest( + username=user.username, + credential_policy=sdk.UserRequireCredentialsPolicy( + ssh=[ + sdk.CredentialKind.PUBLICKEY, + sdk.CredentialKind.TOTP, + sdk.CredentialKind.WEBUSERAPPROVAL, + ], + ), + ), + ) + api.add_user_role(user.id, role.id) + ssh_target = api.create_target( + sdk.TargetDataRequest( + name=f"ssh-{uuid4()}", + options=sdk.TargetOptions( + sdk.TargetOptionsTargetSSHOptions( + kind="Ssh", + host="localhost", + port=ssh_port, + username="root", + auth=sdk.SSHTargetAuth( + sdk.SSHTargetAuthSshTargetPublicKeyAuth(kind="PublicKey") + ), + ) + ), + ) + ) + api.add_target_role(ssh_target.id, role.id) + + totp = pyotp.TOTP(otp_key_base32) + + # Temp files for signaling between the expect script and this async task. + # round2_ready: expect writes this after seeing the round-2 "Press Enter" prompt. + # web_approved: Python writes this after approving browser auth. + tmpdir = Path(tempfile.mkdtemp()) + round2_ready_flag = tmpdir / "round2_ready" + web_approved_flag = tmpdir / "web_approved" + + script = dedent( + f""" + set timeout {timeout - 5} + + spawn ssh {user.username}:{ssh_target.name}@localhost \ + -p {shared_wg.ssh_port} \ + -o StrictHostKeychecking=no \ + -o UserKnownHostsFile=/dev/null \ + -o IdentitiesOnly=yes \ + -o IdentityFile=ssh-keys/id_ed25519 \ + -o PreferredAuthentications=publickey,keyboard-interactive \ + ls /bin/sh + + # Round 1 — both OTP and web approval prompts must appear. + expect "One-time password:" + sleep 0.5 + send "{totp.now()}\\r" + + expect "Press Enter when done:" + send "\\r" + + # Round 2 — only the web approval prompt must appear, NOT the OTP prompt. + # Matching "One-time password:" here is a test failure (exit 10). + expect {{ + "One-time password:" {{ exit 10 }} + "Press Enter when done:" {{ }} + }} + + # Signal Python that the round-2 prompt has been seen. + set fh [open "{round2_ready_flag}" w] + close $fh + + # Wait for Python to approve browser auth before sending Enter. + while {{![file exists "{web_approved_flag}"]}} {{ + sleep 0.1 + }} + + send "\\r" + + expect {{ + "/bin/sh" {{ exit 0 }} + eof {{ exit 1 }} + }} + """ + ) + + # Log in via HTTP to establish a session that can approve web auth requests. + session = aiohttp.ClientSession() + try: + headers = {"Host": f"localhost:{shared_wg.http_port}"} + await session.post( + f"{url}/@warpgate/api/auth/login", + json={"username": user.username, "password": "123"}, + headers=headers, + ssl=False, + ) + ws = await session.ws_connect( + url.replace("https:", "wss:") + "/@warpgate/api/auth/web-auth-requests/stream", + ssl=False, + ) + + expect_proc = processes.start( + ["expect"], + stdin=subprocess.PIPE, + stdout=subprocess.PIPE, + stderr=subprocess.PIPE, + ) + # Write the script now so expect starts running immediately. + # Null out stdin afterwards so communicate() doesn't try to flush + # the already-closed pipe. + expect_proc.stdin.write(script.encode()) + expect_proc.stdin.close() + expect_proc.stdin = None + + # Receive the first web-auth notification (sent when round 1 starts). + msg = await ws.receive(timeout) + auth_id = msg.data + + # Poll until the expect script signals that the round-2 prompt is visible. + while not round2_ready_flag.exists(): + await asyncio.sleep(0.1) + + # Verify the pending auth state before approving. + auth_state_resp = await session.get( + f"{url}/@warpgate/api/auth/state/{auth_id}", ssl=False + ) + auth_state = await auth_state_resp.json() + assert auth_state["protocol"] == "SSH" + assert auth_state["state"] == "WebUserApprovalNeeded" + + # Approve browser auth. + r = await session.post( + f"{url}/@warpgate/api/auth/state/{auth_id}/approve", ssl=False + ) + assert r.status == 200 + + # Unblock the expect script so it can send Enter and complete. + web_approved_flag.touch() + + output, stderr_out = expect_proc.communicate(timeout=timeout) + assert expect_proc.returncode == 0, output + stderr_out + finally: + await session.close() diff --git a/warpgate-protocol-ssh/Cargo.toml b/warpgate-protocol-ssh/Cargo.toml index a83749905..f3c495b2b 100644 --- a/warpgate-protocol-ssh/Cargo.toml +++ b/warpgate-protocol-ssh/Cargo.toml @@ -23,6 +23,7 @@ time = { version = "0.3", default-features = false } tokio.workspace = true tracing.workspace = true uuid.workspace = true +url.workspace = true warpgate-aws = { path = "../warpgate-aws", default-features = false } warpgate-common = { path = "../warpgate-common", default-features = false } warpgate-common-http = { path = "../warpgate-common-http", default-features = false } diff --git a/warpgate-protocol-ssh/src/server/russh_handler.rs b/warpgate-protocol-ssh/src/server/russh_handler.rs index 43008ab8b..c02b54c3f 100644 --- a/warpgate-protocol-ssh/src/server/russh_handler.rs +++ b/warpgate-protocol-ssh/src/server/russh_handler.rs @@ -30,11 +30,7 @@ pub enum ServerHandlerEvent { AuthPublicKey(Secret, PublicKey, oneshot::Sender), AuthPublicKeyOffer(Secret, PublicKey, oneshot::Sender), AuthPassword(Secret, Secret, oneshot::Sender), - AuthKeyboardInteractive( - Secret, - Option>, - oneshot::Sender, - ), + AuthKeyboardInteractive(Secret, Vec>, oneshot::Sender), Data(ServerChannelId, Bytes, oneshot::Sender<()>), ExtendedData(ServerChannelId, Bytes, u32, oneshot::Sender<()>), ChannelClose(ServerChannelId, oneshot::Sender<()>), @@ -231,9 +227,13 @@ impl russh::server::Handler for ServerHandler { ) -> Result { let user = Secret::new(user.to_string()); let response = response - .and_then(|mut r| r.next()) - .and_then(|b| String::from_utf8(b.to_vec()).ok()) - .map(Secret::new); + .map(|response| { + response + .filter_map(|b| String::from_utf8(b.to_vec()).ok()) + .map(Secret::new) + .collect::>() + }) + .unwrap_or_default(); let (tx, rx) = oneshot::channel(); diff --git a/warpgate-protocol-ssh/src/server/session.rs b/warpgate-protocol-ssh/src/server/session.rs index 44f5a7375..41df90a9f 100644 --- a/warpgate-protocol-ssh/src/server/session.rs +++ b/warpgate-protocol-ssh/src/server/session.rs @@ -1,4 +1,3 @@ -use std::borrow::Cow; use std::collections::hash_map::Entry::Vacant; use std::collections::{HashMap, HashSet}; use std::net::{Ipv4Addr, SocketAddr}; @@ -17,6 +16,7 @@ use termcolor::Color; use tokio::sync::mpsc::{UnboundedReceiver, UnboundedSender}; use tokio::sync::{Mutex, broadcast, oneshot}; use tracing::*; +use url::Url; use uuid::Uuid; use warpgate_common::auth::{ AuthCredential, AuthResult, AuthSelector, AuthState, AuthStateUserInfo, CredentialKind, @@ -65,10 +65,9 @@ enum Event { Client(RCEvent), } -enum KeyboardInteractiveState { - None, - OtpRequested, - WebAuthRequested(broadcast::Receiver), +struct PendingKeyboardInteractiveAuth { + otp_prompt_sent: bool, + web_approval_retry_count: Option, } struct CachedSuccessfulTicketAuth { @@ -106,7 +105,7 @@ pub struct ServerSession { service_output: ServiceOutput, channel_writer: ChannelWriter, auth_state: Option>>, - keyboard_interactive_state: KeyboardInteractiveState, + keyboard_interactive_state: Option, cached_successful_ticket_auth: Option, allowed_auth_methods: MethodSet, } @@ -115,6 +114,22 @@ fn session_debug_tag(id: &SessionId, remote_address: &SocketAddr) -> String { format!("[{id} - {remote_address}]") } +fn format_web_auth_instructions(login_url: Option, identification_string: &str) -> String { + let spaced_key = identification_string + .chars() + .map(|c| c.to_string()) + .collect::>() + .join(" "); + let url_line = login_url.map(|u| format!("{u}\n")).unwrap_or_default(); + format!( + "-----------------------------------------------------------------------\n\ + Please verify the SSH authentication request in your browser.\n\ + {url_line}\n\ + Make sure you're seeing this security key: {spaced_key}\n\ + -----------------------------------------------------------------------\n" + ) +} + impl std::fmt::Debug for ServerSession { fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { write!(f, "{}", session_debug_tag(&self.id, &self.remote_address)) @@ -165,7 +180,7 @@ impl ServerSession { service_output: ServiceOutput::new(), channel_writer: ChannelWriter::new(), auth_state: None, - keyboard_interactive_state: KeyboardInteractiveState::None, + keyboard_interactive_state: None, cached_successful_ticket_auth: None, allowed_auth_methods: get_allowed_auth_methods(services).await?, }; @@ -523,8 +538,8 @@ impl ServerSession { let _ = reply.send(self._auth_password(username, password).await); } - ServerHandlerEvent::AuthKeyboardInteractive(username, response, reply) => { - let _ = reply.send(self._auth_keyboard_interactive(username, response).await?); + ServerHandlerEvent::AuthKeyboardInteractive(username, responses, reply) => { + let _ = reply.send(self._auth_keyboard_interactive(username, responses).await?); } ServerHandlerEvent::Data(channel, data, reply) => { @@ -1497,7 +1512,7 @@ impl ServerSession { async fn _auth_keyboard_interactive( &mut self, ssh_username: Secret, - response: Option>, + responses: Vec>, ) -> Result { let selector: AuthSelector = ssh_username.expose_secret().into(); info!("Keyboard-interactive auth as {:?}", selector); @@ -1510,88 +1525,88 @@ impl ServerSession { return Ok(russh::server::Auth::reject()); } - let cred; - match &mut self.keyboard_interactive_state { - KeyboardInteractiveState::None => { - cred = None; - } - KeyboardInteractiveState::OtpRequested => { - cred = response.map(AuthCredential::Otp); - } - KeyboardInteractiveState::WebAuthRequested(event) => { - cred = None; - let _ = event.recv().await; - // the auth state has been updated by now + let keyboard_interactive_state = self.keyboard_interactive_state.take(); + let maybe_otp_cred = keyboard_interactive_state.as_ref().and_then(|s| { + if s.otp_prompt_sent { + responses.into_iter().next().map(AuthCredential::Otp) + } else { + None } - } - - self.keyboard_interactive_state = KeyboardInteractiveState::None; + }); + let pending_web_auth_retries = + keyboard_interactive_state.and_then(|s| s.web_approval_retry_count); - Ok(match self.try_auth_lazy(&selector, cred).await { + Ok(match self.try_auth_lazy(&selector, maybe_otp_cred).await { Ok(AuthResult::Accepted { .. }) => russh::server::Auth::Accept, Ok(AuthResult::Rejected) => russh::server::Auth::reject(), Ok(AuthResult::Need(kinds)) => { + let mut auth_name = "Warpgate authentication".to_string(); + let mut auth_instructions = String::new(); + let mut auth_prompts = vec![]; + + let Some(auth_state) = self.auth_state.as_ref() else { + return Ok(russh::server::Auth::Reject { + proceed_with_methods: None, + partial_success: false, + }); + }; + + let mut next_pending = PendingKeyboardInteractiveAuth { + otp_prompt_sent: false, + web_approval_retry_count: None, + }; + if kinds.contains(&CredentialKind::Totp) { - self.keyboard_interactive_state = KeyboardInteractiveState::OtpRequested; - russh::server::Auth::Partial { - name: Cow::Borrowed("Two-factor authentication"), - instructions: Cow::Borrowed(""), - prompts: Cow::Owned(vec![(Cow::Borrowed("One-time password: "), true)]), - } - } else if kinds.contains(&CredentialKind::WebUserApproval) { - let Some(auth_state) = self.auth_state.as_ref() else { - return Ok(russh::server::Auth::Reject { - proceed_with_methods: None, - partial_success: false, - }); - }; + next_pending.otp_prompt_sent = true; + auth_name = "Two-factor authentication".into(); + auth_prompts.push(("One-time password: ".into(), true)); + } + + if kinds.contains(&CredentialKind::WebUserApproval) { let identification_string = auth_state.lock().await.identification_string().to_owned(); - let auth_state_id = *auth_state.lock().await.id(); - let event = self - .services - .auth_state_store - .lock() - .await - .subscribe(auth_state_id); - self.keyboard_interactive_state = - KeyboardInteractiveState::WebAuthRequested(event); - - let login_url = match construct_external_url( - None, - &*self.services.config.lock().await, - None, - ) - .await - { - Ok(ext_url) => auth_state.lock().await.construct_web_approval_url(ext_url), - Err(error) => { - error!(?error, "Failed to construct external URL"); - return Ok(russh::server::Auth::Reject { - proceed_with_methods: None, - partial_success: false, - }); + + let ext_url = + construct_external_url(None, &*self.services.config.lock().await, None) + .await + .inspect_err(|error| { + warn!(?error, "Failed to construct external URL"); + }) + .ok(); + + let auth_state = auth_state.lock().await; + let login_url = + ext_url.map(|ext_url| auth_state.construct_web_approval_url(ext_url)); + + auth_instructions.push_str(&format_web_auth_instructions( + login_url, + &identification_string, + )); + auth_prompts.push(("Press Enter when done: ".into(), true)); + + const MAX_RETRIES: u8 = 3; + if let Some(retries) = pending_web_auth_retries { + if retries >= MAX_RETRIES { + drop(auth_state); + self.auth_state = None; + return Ok(russh::server::Auth::reject()); } + + auth_instructions.push_str( + "\n[!] Browser authentication was not confirmed, please try again.\n", + ); + next_pending.web_approval_retry_count = Some(retries + 1); + } else { + next_pending.web_approval_retry_count = Some(0); }; + } + if !auth_prompts.is_empty() { + self.keyboard_interactive_state = Some(next_pending); russh::server::Auth::Partial { - name: Cow::Borrowed("Warpgate authentication"), - instructions: Cow::Owned(format!( - concat!( - "-----------------------------------------------------------------------\n", - "Warpgate authentication: please open the following URL in your browser:\n", - "{}\n\n", - "Make sure you're seeing this security key: {}\n", - "-----------------------------------------------------------------------\n" - ), - login_url, - identification_string - .chars() - .map(|x| x.to_string()) - .collect::>() - .join(" ") - )), - prompts: Cow::Owned(vec![(Cow::Borrowed("Press Enter when done: "), true)]), + name: auth_name.into(), + instructions: auth_instructions.into(), + prompts: auth_prompts.into(), } } else { russh::server::Auth::Reject { From eca44fb91bf6545b2b40ff481e5ef3fed07d0eb4 Mon Sep 17 00:00:00 2001 From: Eugene Date: Mon, 25 May 2026 21:46:36 +0200 Subject: [PATCH 106/556] fix section link color in light mode --- warpgate-web/src/theme/vars.light.scss | 2 ++ 1 file changed, 2 insertions(+) diff --git a/warpgate-web/src/theme/vars.light.scss b/warpgate-web/src/theme/vars.light.scss index daa207bf0..9893a550b 100644 --- a/warpgate-web/src/theme/vars.light.scss +++ b/warpgate-web/src/theme/vars.light.scss @@ -59,3 +59,5 @@ $success-text-emphasis: shade-color($success, 10%); $info-text-emphasis: shade-color($info, 10%); $warning-text-emphasis: shade-color($warning, 10%); $danger-text-emphasis: shade-color($danger, 10%); + +$nav-pills-link-active-color: $body-color; From 50d4938a82e920ce9c64cdc6ccb20614aea975cf Mon Sep 17 00:00:00 2001 From: SteezyCougar <34529175+SteezyCougar@users.noreply.github.com> Date: Tue, 26 May 2026 04:32:39 -0600 Subject: [PATCH 107/556] Little/max api token duration (#1946) We have two use-case that this PR solves 1. We want to ensure no token can last longer than we set our max to be 2. We use a local development tool that we want to make it easy for them to create a token so that the tool can grant them access easier to resources without them having to be involved (Hence why we want to be able to restrict the valid timeframe of said tokens) --------- Co-authored-by: Eugene --- warpgate-admin/src/api/parameters.rs | 5 +++ warpgate-db-entities/src/Parameters.rs | 2 + warpgate-db-migrations/src/lib.rs | 2 + .../src/m00046_max_api_token_duration.rs | 39 +++++++++++++++++++ warpgate-protocol-http/src/api/api_tokens.rs | 16 +++++++- warpgate-protocol-http/src/api/info.rs | 2 + .../src/admin/config/Parameters.svelte | 27 +++++++------ .../src/admin/config/targets/Target.svelte | 13 +------ .../src/admin/lib/openapi-schema.json | 10 ++++- warpgate-web/src/common/duration.ts | 22 +++++++++++ .../src/gateway/ApiTokenManager.svelte | 33 ++++++++++++++-- .../src/gateway/CreateApiTokenModal.svelte | 26 ++++++++++++- .../src/gateway/lib/openapi-schema.json | 19 ++++++++- 13 files changed, 182 insertions(+), 34 deletions(-) create mode 100644 warpgate-db-migrations/src/m00046_max_api_token_duration.rs diff --git a/warpgate-admin/src/api/parameters.rs b/warpgate-admin/src/api/parameters.rs index d110f2a4d..4c99940f9 100644 --- a/warpgate-admin/src/api/parameters.rs +++ b/warpgate-admin/src/api/parameters.rs @@ -28,6 +28,7 @@ struct ParameterValues { pub ticket_require_description: bool, pub ticket_request_show_all_targets: bool, pub show_session_menu: bool, + pub max_api_token_duration_seconds: Option, } #[derive(Serialize, Object)] @@ -45,6 +46,7 @@ struct ParameterUpdate { pub ticket_require_description: Option, pub ticket_request_show_all_targets: Option, pub show_session_menu: Option, + pub max_api_token_duration_seconds: Option>, } #[derive(ApiResponse)] @@ -86,6 +88,7 @@ impl Api { ticket_require_description: parameters.ticket_require_description, ticket_request_show_all_targets: parameters.ticket_request_show_all_targets, show_session_menu: parameters.show_session_menu, + max_api_token_duration_seconds: parameters.max_api_token_duration_seconds, }))) } @@ -126,6 +129,8 @@ impl Api { parameters.ticket_request_show_all_targets = body.ticket_request_show_all_targets.map_or(NotSet, Set); parameters.show_session_menu = body.show_session_menu.map_or(NotSet, Set); + parameters.max_api_token_duration_seconds = + body.max_api_token_duration_seconds.map_or(NotSet, Set); Parameters::Entity::update(parameters).exec(&*db).await?; drop(db); diff --git a/warpgate-db-entities/src/Parameters.rs b/warpgate-db-entities/src/Parameters.rs index e28ac2fa3..c869623c3 100644 --- a/warpgate-db-entities/src/Parameters.rs +++ b/warpgate-db-entities/src/Parameters.rs @@ -24,6 +24,7 @@ pub struct Model { pub ticket_require_description: bool, pub ticket_request_show_all_targets: bool, pub show_session_menu: bool, + pub max_api_token_duration_seconds: Option, } impl ActiveModelBehavior for ActiveModel {} @@ -53,6 +54,7 @@ impl Entity { ticket_require_description: Set(false), ticket_request_show_all_targets: Set(false), show_session_menu: Set(true), + max_api_token_duration_seconds: Set(None), } .insert(db) .await diff --git a/warpgate-db-migrations/src/lib.rs b/warpgate-db-migrations/src/lib.rs index 24a76a366..9be92a095 100644 --- a/warpgate-db-migrations/src/lib.rs +++ b/warpgate-db-migrations/src/lib.rs @@ -47,6 +47,7 @@ mod m00042_database_target_auth_again; mod m00043_unique_usernames; mod m00044_ticket_requests; mod m00045_role_default_flag; +mod m00046_max_api_token_duration; pub struct Migrator; @@ -99,6 +100,7 @@ impl MigratorTrait for Migrator { Box::new(m00043_unique_usernames::Migration), Box::new(m00044_ticket_requests::Migration), Box::new(m00045_role_default_flag::Migration), + Box::new(m00046_max_api_token_duration::Migration), ] } } diff --git a/warpgate-db-migrations/src/m00046_max_api_token_duration.rs b/warpgate-db-migrations/src/m00046_max_api_token_duration.rs new file mode 100644 index 000000000..d1efd39d4 --- /dev/null +++ b/warpgate-db-migrations/src/m00046_max_api_token_duration.rs @@ -0,0 +1,39 @@ +use sea_orm_migration::prelude::*; + +use crate::m00010_parameters::parameters; + +#[derive(DeriveMigrationName)] +pub struct Migration; + +#[async_trait::async_trait] +impl MigrationTrait for Migration { + async fn up(&self, manager: &SchemaManager) -> Result<(), DbErr> { + manager + .alter_table( + Table::alter() + .table(parameters::Entity) + .add_column( + ColumnDef::new(Alias::new("max_api_token_duration_seconds")) + .big_integer() + .null(), + ) + .to_owned(), + ) + .await?; + + Ok(()) + } + + async fn down(&self, manager: &SchemaManager) -> Result<(), DbErr> { + manager + .alter_table( + Table::alter() + .table(parameters::Entity) + .drop_column(Alias::new("max_api_token_duration_seconds")) + .to_owned(), + ) + .await?; + + Ok(()) + } +} diff --git a/warpgate-protocol-http/src/api/api_tokens.rs b/warpgate-protocol-http/src/api/api_tokens.rs index 845daa9cc..b00c60b5a 100644 --- a/warpgate-protocol-http/src/api/api_tokens.rs +++ b/warpgate-protocol-http/src/api/api_tokens.rs @@ -3,12 +3,12 @@ use poem_openapi::param::Path; use poem_openapi::payload::Json; use poem_openapi::{ApiResponse, Object, OpenApi}; use sea_orm::{ActiveModelTrait, ColumnTrait, ModelTrait, QueryFilter, Set}; -use time::OffsetDateTime; +use time::{Duration, OffsetDateTime}; use uuid::Uuid; use warpgate_common::WarpgateError; use warpgate_common::helpers::hash::generate_ticket_secret; use warpgate_common_http::auth::AuthenticatedRequestContext; -use warpgate_db_entities::ApiToken; +use warpgate_db_entities::{ApiToken, Parameters}; use super::common::get_user; use crate::common::endpoint_auth; @@ -58,6 +58,8 @@ struct TokenAndSecret { enum CreateApiTokenResponse { #[oai(status = 201)] Created(Json), + #[oai(status = 400)] + BadRequest(Json), #[oai(status = 401)] Unauthorized, } @@ -116,6 +118,16 @@ impl Api { return Ok(CreateApiTokenResponse::Unauthorized); }; + let parameters = Parameters::Entity::get(&db).await?; + if let Some(max_seconds) = parameters.max_api_token_duration_seconds { + let max_expiry = OffsetDateTime::now_utc() + Duration::seconds(max_seconds); + if body.expiry > max_expiry { + return Ok(CreateApiTokenResponse::BadRequest(Json(format!( + "Token expiry exceeds maximum allowed duration of {max_seconds} seconds" + )))); + } + } + let secret = generate_ticket_secret(); let object = ApiToken::ActiveModel { id: Set(Uuid::new_v4()), diff --git a/warpgate-protocol-http/src/api/info.rs b/warpgate-protocol-http/src/api/info.rs index 7bdb2bd84..85cb17a39 100644 --- a/warpgate-protocol-http/src/api/info.rs +++ b/warpgate-protocol-http/src/api/info.rs @@ -92,6 +92,7 @@ pub struct Info { ticket_max_uses: Option, ticket_require_description: bool, ticket_request_show_all_targets: bool, + max_api_token_duration_seconds: Option, has_ldap: bool, setup_state: Option, admin_permissions: Option, @@ -299,6 +300,7 @@ impl Api { ticket_max_uses: parameters.ticket_max_uses, ticket_require_description: parameters.ticket_require_description, ticket_request_show_all_targets: parameters.ticket_request_show_all_targets, + max_api_token_duration_seconds: parameters.max_api_token_duration_seconds, setup_state, has_ldap: auth_ctx.is_some() && has_ldap, admin_permissions, diff --git a/warpgate-web/src/admin/config/Parameters.svelte b/warpgate-web/src/admin/config/Parameters.svelte index 618a8f94f..ab79061aa 100644 --- a/warpgate-web/src/admin/config/Parameters.svelte +++ b/warpgate-web/src/admin/config/Parameters.svelte @@ -6,22 +6,17 @@ import RateLimitInput from 'common/RateLimitInput.svelte' import InfoBox from 'common/InfoBox.svelte' import PermissionGate from 'admin/lib/PermissionGate.svelte' - import { formatDurationAsHumantime, parseHumantimeDuration } from 'common/duration' + import { humantimeDuration } from 'common/duration' import { reloadServerInfo } from 'gateway/lib/store' let parameters: ParameterValues | undefined = $state() let hasSsoProviders = $state(false) const initPromise = init() - let durationText = $state('') - async function init () { parameters = await api.getParameters({}) const ssoProviders = await gatewayApi.getSsoProviders() hasSsoProviders = ssoProviders.length > 0 - durationText = parameters.ticketMaxDurationSeconds - ? formatDurationAsHumantime(parameters.ticketMaxDurationSeconds) - : '' } async function update() { @@ -30,12 +25,6 @@ }) await reloadServerInfo() } - - function onDurationChange () { - const seconds = parseHumantimeDuration(durationText) - parameters!.ticketMaxDurationSeconds = seconds ?? undefined - update() - }
@@ -202,8 +191,7 @@ type="text" class="form-control" placeholder="e.g. 8h, 30m, 1d" - bind:value={durationText} - onchange={onDurationChange} + use:humantimeDuration={{ seconds: parameters.ticketMaxDurationSeconds, onChange: v => { parameters!.ticketMaxDurationSeconds = v; update() } }} /> Global default. Can be overridden per target. Examples: 30m, 8h, 1d, 2h30m. @@ -226,6 +214,17 @@ {/if} +

API tokens

+ + + { parameters!.maxApiTokenDurationSeconds = v; update() } }} + /> + +

HTTP

+{#if error} +{error} +{/if} + {#if lastCreatedSecret}
Your token - shown only once:
@@ -84,5 +109,7 @@ {/if} diff --git a/warpgate-web/src/gateway/CreateApiTokenModal.svelte b/warpgate-web/src/gateway/CreateApiTokenModal.svelte index 27cc12e83..73bc461b6 100644 --- a/warpgate-web/src/gateway/CreateApiTokenModal.svelte +++ b/warpgate-web/src/gateway/CreateApiTokenModal.svelte @@ -9,19 +9,35 @@ ModalFooter, } from '@sveltestrap/sveltestrap' + import { serverInfo } from 'gateway/lib/store' import ModalHeader from 'common/sveltestrap-s5-ports/ModalHeader.svelte' interface Props { isOpen: boolean create: (label: string, expiry: Date) => void + initialLabel?: string + initialExpiryMs?: number } + let defaultDurationMs = 1000 * 60 * 60 * 24 * 7 + const maxDurationMs = $serverInfo?.maxApiTokenDurationSeconds ? ($serverInfo.maxApiTokenDurationSeconds * 1000) : null + defaultDurationMs = maxDurationMs ? Math.min(maxDurationMs, defaultDurationMs) : defaultDurationMs + let { isOpen = $bindable(true), create, + initialLabel = '', + initialExpiryMs = defaultDurationMs, }: Props = $props() - let label = $state('') - let expiry = $state(new Date(Date.now() + 1000 * 60 * 60 * 24 * 7).toISOString()) + + let validatedInitialExpiryMs = $derived(maxDurationMs ? Math.min(initialExpiryMs, maxDurationMs) : initialExpiryMs) + + // svelte-ignore state_referenced_locally + let label = $state(initialLabel) + // svelte-ignore state_referenced_locally + let expiry = $state(new Date(Date.now() + validatedInitialExpiryMs).toISOString().slice(0, 16)) + let maxExpiryDate = $derived(maxDurationMs ? new Date(Date.now() + maxDurationMs) : undefined) + let maxExpiry = $derived(maxExpiryDate?.toISOString().slice(0, 16)) let field: HTMLInputElement|undefined = $state() let validated = $state(false) @@ -55,7 +71,13 @@ + {#if maxDurationMs !== Number.POSITIVE_INFINITY} + + Maximum: {Math.floor(maxDurationMs / 86400 / 1000)} days + + {/if} diff --git a/warpgate-web/src/gateway/lib/openapi-schema.json b/warpgate-web/src/gateway/lib/openapi-schema.json index 1d59beb5a..2582ec761 100644 --- a/warpgate-web/src/gateway/lib/openapi-schema.json +++ b/warpgate-web/src/gateway/lib/openapi-schema.json @@ -2,7 +2,7 @@ "openapi": "3.0.0", "info": { "title": "Warpgate HTTP proxy", - "version": "v0.23.4-21-ge437f0a5-modified" + "version": "v0.23.4-33-gf91c3fd4-modified" }, "servers": [ { @@ -785,6 +785,16 @@ } } }, + "400": { + "description": "", + "content": { + "application/json; charset=utf-8": { + "schema": { + "type": "string" + } + } + } + }, "401": { "description": "" } @@ -1153,6 +1163,9 @@ }, "404": { "description": "" + }, + "429": { + "description": "" } }, "security": [ @@ -1738,6 +1751,10 @@ "ticket_request_show_all_targets": { "type": "boolean" }, + "max_api_token_duration_seconds": { + "type": "integer", + "format": "int64" + }, "has_ldap": { "type": "boolean" }, From 71a8f9b3494b53a7ee4752c9b8f61c8ac3a0e1e9 Mon Sep 17 00:00:00 2001 From: Eugene Date: Tue, 26 May 2026 12:57:48 +0200 Subject: [PATCH 108/556] enable svelte dev build --- warpgate-web/package.json | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/warpgate-web/package.json b/warpgate-web/package.json index 171ab7872..970c06cb0 100644 --- a/warpgate-web/package.json +++ b/warpgate-web/package.json @@ -5,8 +5,8 @@ "type": "module", "scripts": { "build": "vite build", - "devbuild": "vite build --mode development --minify false", - "watch": "vite build -w --mode development --minify false", + "devbuild": "NODE_ENV=development vite build --mode development --minify false", + "watch": "NODE_ENV=development vite build -w --mode development --minify false", "check": "svelte-check --compiler-warnings 'a11y-no-noninteractive-element-interactions:ignore,a11y-click-events-have-key-events:ignore,a11y-no-static-element-interactions:ignore' --tsconfig ./tsconfig.json", "lint": "eslint src && svelte-check", "postinstall": "npm run openapi:client:gateway && npm run openapi:client:admin", From 5616e59436b120efca8d0b4734dfe0aaa28e5de3 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Tue, 26 May 2026 15:34:59 +0200 Subject: [PATCH 109/556] Bump docker/setup-buildx-action from 4.0.0 to 4.1.0 (#1973) Signed-off-by: dependabot[bot] --- .github/workflows/docker.yml | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/.github/workflows/docker.yml b/.github/workflows/docker.yml index 98cca4eb3..2c7816920 100644 --- a/.github/workflows/docker.yml +++ b/.github/workflows/docker.yml @@ -43,7 +43,7 @@ jobs: uses: docker/setup-qemu-action@ce360397dd3f832beb865e1373c09c0e9f86d70a - name: Set up Docker Buildx - uses: docker/setup-buildx-action@4d04d5d9486b7bd6fa91e7baf45bbb4f8b9deedd + uses: docker/setup-buildx-action@d7f5e7f509e45cec5c76c4d5afdd7de93d0b3df5 - name: Log into registry ${{ env.REGISTRY }} if: github.event_name != 'pull_request' || github.event.pull_request.head.repo.full_name == github.repository @@ -124,7 +124,7 @@ jobs: merge-multiple: true - name: Set up Docker Buildx - uses: docker/setup-buildx-action@4d04d5d9486b7bd6fa91e7baf45bbb4f8b9deedd + uses: docker/setup-buildx-action@d7f5e7f509e45cec5c76c4d5afdd7de93d0b3df5 - name: Log into registry ${{ env.REGISTRY }} uses: docker/login-action@650006c6eb7dba73a995cc03b0b2d7f5ca915bee From 182bb7f2a51c7c057bf6c450370e3e12bd51d88f Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Tue, 26 May 2026 15:35:02 +0200 Subject: [PATCH 110/556] Bump pytest-asyncio from 0.26.0 to 1.3.0 in /tests (#1926) Signed-off-by: dependabot[bot] --- tests/poetry.lock | 27 +++++++++++++++++++++------ tests/pyproject.toml | 2 +- 2 files changed, 22 insertions(+), 7 deletions(-) diff --git a/tests/poetry.lock b/tests/poetry.lock index bef88ad53..105804835 100644 --- a/tests/poetry.lock +++ b/tests/poetry.lock @@ -208,6 +208,19 @@ files = [ {file = "attrs-26.1.0.tar.gz", hash = "sha256:d03ceb89cb322a8fd706d4fb91940737b6642aa36998fe130a9bc96c985eff32"}, ] +[[package]] +name = "backports-asyncio-runner" +version = "1.2.0" +description = "Backport of asyncio.Runner, a context manager that controls event loop life cycle." +optional = false +python-versions = "<3.11,>=3.8" +groups = ["dev"] +markers = "python_version == \"3.10\"" +files = [ + {file = "backports_asyncio_runner-1.2.0-py3-none-any.whl", hash = "sha256:0da0a936a8aeb554eccb426dc55af3ba63bcdc69fa1a600b5bb305413a4477b5"}, + {file = "backports_asyncio_runner-1.2.0.tar.gz", hash = "sha256:a5aa7b2b7d8f8bfcaa2b57313f70792df84e32a2a746f585213373f900b42162"}, +] + [[package]] name = "bcrypt" version = "5.0.0" @@ -1874,18 +1887,20 @@ dev = ["argcomplete", "attrs (>=19.2)", "hypothesis (>=3.56)", "mock", "requests [[package]] name = "pytest-asyncio" -version = "0.26.0" +version = "1.3.0" description = "Pytest support for asyncio" optional = false -python-versions = ">=3.9" +python-versions = ">=3.10" groups = ["dev"] files = [ - {file = "pytest_asyncio-0.26.0-py3-none-any.whl", hash = "sha256:7b51ed894f4fbea1340262bdae5135797ebbe21d8638978e35d31c6d19f72fb0"}, - {file = "pytest_asyncio-0.26.0.tar.gz", hash = "sha256:c4df2a697648241ff39e7f0e4a73050b03f123f760673956cf0d72a4990e312f"}, + {file = "pytest_asyncio-1.3.0-py3-none-any.whl", hash = "sha256:611e26147c7f77640e6d0a92a38ed17c3e9848063698d5c93d5aa7aa11cebff5"}, + {file = "pytest_asyncio-1.3.0.tar.gz", hash = "sha256:d7f52f36d231b80ee124cd216ffb19369aa168fc10095013c6b014a34d3ee9e5"}, ] [package.dependencies] -pytest = ">=8.2,<9" +backports-asyncio-runner = {version = ">=1.1,<2", markers = "python_version < \"3.11\""} +pytest = ">=8.2,<10" +typing-extensions = {version = ">=4.12", markers = "python_version < \"3.13\""} [package.extras] docs = ["sphinx (>=5.3)", "sphinx-rtd-theme (>=1)"] @@ -2411,4 +2426,4 @@ propcache = ">=0.2.1" [metadata] lock-version = "2.1" python-versions = "^3.10" -content-hash = "fe6e9f9cbe5278175aaf55d2e20e8f373574cddb20293a0ec4d9ab87f94baa05" +content-hash = "e49a4aab48e9bf1ff092f63190f5ec91aca4236b0c0d6ab61278ef9fbe7a1199" diff --git a/tests/pyproject.toml b/tests/pyproject.toml index a46a09000..7416b07b0 100644 --- a/tests/pyproject.toml +++ b/tests/pyproject.toml @@ -25,7 +25,7 @@ flake8 = "^7.3.0" black = "^26" [tool.poetry.group.dev.dependencies] -pytest-asyncio = "^0.26.0" +pytest-asyncio = ">=0.26,<1.4" pytest-timeout = "^2.4.0" pyright = "^1.1.408" From 55407fede51b20f8759c69c77b90b36b32e4f40f Mon Sep 17 00:00:00 2001 From: Eugene Date: Tue, 26 May 2026 16:07:58 +0200 Subject: [PATCH 111/556] fixed #1945 - make SCP recording optional (#1978) --- warpgate-admin/src/api/parameters.rs | 4 ++ warpgate-db-entities/src/Parameters.rs | 2 + warpgate-db-migrations/src/lib.rs | 2 + .../src/m00047_record_scp.rs | 40 +++++++++++++ warpgate-protocol-ssh/src/server/session.rs | 58 ++++++++++++------- .../src/admin/config/Parameters.svelte | 21 +++++++ 6 files changed, 106 insertions(+), 21 deletions(-) create mode 100644 warpgate-db-migrations/src/m00047_record_scp.rs diff --git a/warpgate-admin/src/api/parameters.rs b/warpgate-admin/src/api/parameters.rs index 4c99940f9..59644cad0 100644 --- a/warpgate-admin/src/api/parameters.rs +++ b/warpgate-admin/src/api/parameters.rs @@ -29,6 +29,7 @@ struct ParameterValues { pub ticket_request_show_all_targets: bool, pub show_session_menu: bool, pub max_api_token_duration_seconds: Option, + pub record_scp: bool, } #[derive(Serialize, Object)] @@ -47,6 +48,7 @@ struct ParameterUpdate { pub ticket_request_show_all_targets: Option, pub show_session_menu: Option, pub max_api_token_duration_seconds: Option>, + pub record_scp: Option, } #[derive(ApiResponse)] @@ -89,6 +91,7 @@ impl Api { ticket_request_show_all_targets: parameters.ticket_request_show_all_targets, show_session_menu: parameters.show_session_menu, max_api_token_duration_seconds: parameters.max_api_token_duration_seconds, + record_scp: parameters.record_scp, }))) } @@ -131,6 +134,7 @@ impl Api { parameters.show_session_menu = body.show_session_menu.map_or(NotSet, Set); parameters.max_api_token_duration_seconds = body.max_api_token_duration_seconds.map_or(NotSet, Set); + parameters.record_scp = body.record_scp.map_or(NotSet, Set); Parameters::Entity::update(parameters).exec(&*db).await?; drop(db); diff --git a/warpgate-db-entities/src/Parameters.rs b/warpgate-db-entities/src/Parameters.rs index c869623c3..a7b99be22 100644 --- a/warpgate-db-entities/src/Parameters.rs +++ b/warpgate-db-entities/src/Parameters.rs @@ -25,6 +25,7 @@ pub struct Model { pub ticket_request_show_all_targets: bool, pub show_session_menu: bool, pub max_api_token_duration_seconds: Option, + pub record_scp: bool, } impl ActiveModelBehavior for ActiveModel {} @@ -55,6 +56,7 @@ impl Entity { ticket_request_show_all_targets: Set(false), show_session_menu: Set(true), max_api_token_duration_seconds: Set(None), + record_scp: Set(true), } .insert(db) .await diff --git a/warpgate-db-migrations/src/lib.rs b/warpgate-db-migrations/src/lib.rs index 9be92a095..11b20ebc1 100644 --- a/warpgate-db-migrations/src/lib.rs +++ b/warpgate-db-migrations/src/lib.rs @@ -48,6 +48,7 @@ mod m00043_unique_usernames; mod m00044_ticket_requests; mod m00045_role_default_flag; mod m00046_max_api_token_duration; +mod m00047_record_scp; pub struct Migrator; @@ -101,6 +102,7 @@ impl MigratorTrait for Migrator { Box::new(m00044_ticket_requests::Migration), Box::new(m00045_role_default_flag::Migration), Box::new(m00046_max_api_token_duration::Migration), + Box::new(m00047_record_scp::Migration), ] } } diff --git a/warpgate-db-migrations/src/m00047_record_scp.rs b/warpgate-db-migrations/src/m00047_record_scp.rs new file mode 100644 index 000000000..a801b2e67 --- /dev/null +++ b/warpgate-db-migrations/src/m00047_record_scp.rs @@ -0,0 +1,40 @@ +use sea_orm_migration::prelude::*; + +use crate::m00010_parameters::parameters; + +#[derive(DeriveMigrationName)] +pub struct Migration; + +#[async_trait::async_trait] +impl MigrationTrait for Migration { + async fn up(&self, manager: &SchemaManager) -> Result<(), DbErr> { + manager + .alter_table( + Table::alter() + .table(parameters::Entity) + .add_column( + ColumnDef::new(Alias::new("record_scp")) + .boolean() + .not_null() + .default(true), + ) + .to_owned(), + ) + .await?; + + Ok(()) + } + + async fn down(&self, manager: &SchemaManager) -> Result<(), DbErr> { + manager + .alter_table( + Table::alter() + .table(parameters::Entity) + .drop_column(Alias::new("record_scp")) + .to_owned(), + ) + .await?; + + Ok(()) + } +} diff --git a/warpgate-protocol-ssh/src/server/session.rs b/warpgate-protocol-ssh/src/server/session.rs index 41df90a9f..72e9682fa 100644 --- a/warpgate-protocol-ssh/src/server/session.rs +++ b/warpgate-protocol-ssh/src/server/session.rs @@ -34,6 +34,7 @@ use warpgate_core::recordings::{ use warpgate_core::{ ConfigProvider, Services, WarpgateServerHandle, authorize_ticket, consume_ticket, }; +use warpgate_db_entities::Parameters; use super::channel_writer::ChannelWriter; use super::russh_handler::ServerHandlerEvent; @@ -1159,29 +1160,44 @@ impl ServerSession { data: Bytes, ) -> Result<()> { let channel_id = self.map_channel(server_channel_id)?; - match std::str::from_utf8(&data) { - Err(e) => { - error!(channel=%channel_id, ?data, "Requested exec - invalid UTF-8"); - anyhow::bail!(e) - } - Ok::<&str, _>(command) => { - debug!(channel=%channel_id, %command, "Requested exec"); - let _ = self.maybe_connect_remote().await; - let _ = self.send_command(RCCommand::Channel( - channel_id, - ChannelOperation::RequestExec(command.to_string()), - )); - } - } + let command = std::str::from_utf8(&data).inspect_err(|_| { + error!(channel=%channel_id, ?data, "Requested exec - invalid UTF-8"); + })?; + debug!(channel=%channel_id, %command, "Requested exec"); - self.start_terminal_recording( + let is_scp = command == "scp" || command.starts_with("scp "); + let _ = self.maybe_connect_remote().await; + let _ = self.send_command(RCCommand::Channel( channel_id, - SshRecordingMetadata::Exec { - // HACK russh ChannelId is opaque except via Display - channel: server_channel_id.0.to_string().parse().unwrap_or_default(), - }, - ) - .await; + ChannelOperation::RequestExec(command.to_string()), + )); + + let should_record = if is_scp { + let db = self.services.db.lock().await; + let should_record = Parameters::Entity::get(&*db) + .await + .map(|p| p.record_scp) + .unwrap_or(true); + + if !should_record { + info!(channel=%channel_id, "Not recording SCP exec session, command was '{command}'"); + } + + should_record + } else { + true + }; + + if should_record { + self.start_terminal_recording( + channel_id, + SshRecordingMetadata::Exec { + // HACK russh ChannelId is opaque except via Display + channel: server_channel_id.0.to_string().parse().unwrap_or_default(), + }, + ) + .await; + } Ok(()) } diff --git a/warpgate-web/src/admin/config/Parameters.svelte b/warpgate-web/src/admin/config/Parameters.svelte index ab79061aa..6cf793356 100644 --- a/warpgate-web/src/admin/config/Parameters.svelte +++ b/warpgate-web/src/admin/config/Parameters.svelte @@ -114,6 +114,27 @@ Disabling password authentication can help prevent brute-force attacks. +
+ + + + Legacy SCP works over an exec channel and would be normally recorded like any other command. Disable to prevent SCP recordings from wasting storage space. + +

Self-service tickets

+ {:else if richEntry?._type === 'UserAuthenticated1'} +
+ Authenticated + + {#if richEntry.credentials} + {richEntry.credentials} + {/if} + {#if richEntry.client_ip} + from {richEntry.client_ip} + {/if} +
+ {:else if richEntry?._type === 'UserAuthenticationFailed1'} +
+ Authentication failed + {#if richEntry.user_id} + + {:else} + {richEntry.username} + {/if} + {#if richEntry.credentials} + {richEntry.credentials} + {/if} + {#if richEntry.reason} + {richEntry.reason} + {/if} + {#if richEntry.client_ip} + from {richEntry.client_ip} + {/if} +
{:else if richEntry?._type === 'TargetSessionStarted1'}
Target session started for @@ -530,7 +598,7 @@ function parseRichLogEntry(entry: LogEntry): RichLogEntry | null { {item.text} - {#each Object.entries(item.values ?? {}) as pair (pair[0])} + {#each genericValues(item) as pair (pair[0])} {pair[0]}: {pair[1]} @@ -671,6 +739,22 @@ function parseRichLogEntry(entry: LogEntry): RichLogEntry | null { flex-wrap: wrap; align-items: center; gap: 0.5em; + + .event-label { + font-weight: 700; + } + + &.auth-failed { + .event-label { + color: var(--bs-danger-text-emphasis, #dc3545); + } + + .auth-failed-reason { + background: rgba(var(--bs-danger-rgb, 220, 53, 69), 0.16); + border: 1px solid rgba(var(--bs-danger-rgb, 220, 53, 69), 0.34); + color: var(--bs-danger-text-emphasis, #dc3545); + } + } } } diff --git a/warpgate/src/commands/setup.rs b/warpgate/src/commands/setup.rs index cc205bd1e..8d65c323d 100644 --- a/warpgate/src/commands/setup.rs +++ b/warpgate/src/commands/setup.rs @@ -358,7 +358,6 @@ pub async fn command(cli: &Cli, params: &GlobalParams) -> Result<()> { name: Set(BUILTIN_ADMIN_USERNAME.to_string()), description: Set("".to_string()), is_default: Set(false), - ..Default::default() } .insert(&db) .await?; From db0f15ea98946114f517a44b64b1986f78bec081 Mon Sep 17 00:00:00 2001 From: kamilkrzeminski Date: Sat, 6 Jun 2026 01:32:12 +0200 Subject: [PATCH 139/556] feat: add configurable password policy enforcement (#1882) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Co-authored-by: Kamil Krzemiński Co-authored-by: Eugene --- warpgate-admin/src/api/parameters.rs | 13 +- .../src/api/password_credentials.rs | 18 ++- warpgate-common/src/helpers/mod.rs | 1 + .../src/helpers/password_policy.rs | 117 ++++++++++++++++++ warpgate-common/src/lib.rs | 1 + warpgate-db-entities/src/Parameters.rs | 23 ++++ warpgate-db-migrations/src/lib.rs | 2 + .../src/m00050_password_policy.rs | 100 +++++++++++++++ warpgate-protocol-http/src/api/credentials.rs | 18 ++- .../src/admin/CreatePasswordModal.svelte | 44 ++++++- .../src/admin/config/Parameters.svelte | 76 ++++++++++++ .../src/admin/lib/openapi-schema.json | 64 +++++++++- warpgate-web/src/common/duration.ts | 4 +- .../src/gateway/lib/openapi-schema.json | 66 +++++++++- 14 files changed, 534 insertions(+), 13 deletions(-) create mode 100644 warpgate-common/src/helpers/password_policy.rs create mode 100644 warpgate-db-migrations/src/m00050_password_policy.rs diff --git a/warpgate-admin/src/api/parameters.rs b/warpgate-admin/src/api/parameters.rs index 94dcf53e0..279d594b6 100644 --- a/warpgate-admin/src/api/parameters.rs +++ b/warpgate-admin/src/api/parameters.rs @@ -4,7 +4,7 @@ use poem_openapi::{ApiResponse, Object, OpenApi}; use sea_orm::ActiveValue::NotSet; use sea_orm::{EntityTrait, IntoActiveModel, Set}; use serde::Serialize; -use warpgate_common::{AdminPermission, WarpgateError}; +use warpgate_common::{AdminPermission, PasswordPolicy, WarpgateError}; use warpgate_common_http::AuthenticatedRequestContext; use warpgate_db_entities::Parameters; @@ -29,6 +29,7 @@ struct ParameterValues { pub ticket_request_show_all_targets: bool, pub target_click_action: Parameters::TargetClickAction, pub show_session_menu: bool, + pub password_policy: PasswordPolicy, pub max_api_token_duration_seconds: Option, pub record_scp: bool, } @@ -49,6 +50,7 @@ struct ParameterUpdate { pub ticket_request_show_all_targets: Option, pub target_click_action: Option, pub show_session_menu: Option, + pub password_policy: Option, pub max_api_token_duration_seconds: Option>, pub record_scp: Option, } @@ -93,6 +95,7 @@ impl Api { ticket_request_show_all_targets: parameters.ticket_request_show_all_targets, target_click_action: parameters.target_click_action, show_session_menu: parameters.show_session_menu, + password_policy: parameters.password_policy(), max_api_token_duration_seconds: parameters.max_api_token_duration_seconds, record_scp: parameters.record_scp, }))) @@ -140,6 +143,14 @@ impl Api { body.max_api_token_duration_seconds.map_or(NotSet, Set); parameters.record_scp = body.record_scp.map_or(NotSet, Set); + if let Some(ref policy) = body.password_policy { + parameters.password_policy_min_length = Set(policy.min_length as i32); + parameters.password_policy_require_uppercase = Set(policy.require_uppercase); + parameters.password_policy_require_lowercase = Set(policy.require_lowercase); + parameters.password_policy_require_digits = Set(policy.require_digits); + parameters.password_policy_require_special = Set(policy.require_special); + } + Parameters::Entity::update(parameters).exec(&*db).await?; drop(db); diff --git a/warpgate-admin/src/api/password_credentials.rs b/warpgate-admin/src/api/password_credentials.rs index 15b325bae..37a50f92d 100644 --- a/warpgate-admin/src/api/password_credentials.rs +++ b/warpgate-admin/src/api/password_credentials.rs @@ -4,10 +4,13 @@ use poem_openapi::payload::Json; use poem_openapi::{ApiResponse, Object, OpenApi}; use sea_orm::{ActiveModelTrait, ColumnTrait, EntityTrait, ModelTrait, QueryFilter, Set}; use uuid::Uuid; -use warpgate_common::{AdminPermission, Secret, UserPasswordCredential, WarpgateError}; +use warpgate_common::{ + AdminPermission, PasswordPolicyViolation, Secret, UserPasswordCredential, WarpgateError, + validate_password, +}; use warpgate_common_http::AuthenticatedRequestContext; use warpgate_core::logging::{AuditEvent, CredentialChangedVia}; -use warpgate_db_entities::{PasswordCredential, User}; +use warpgate_db_entities::{Parameters, PasswordCredential, User}; use super::AnySecurityScheme; use crate::api::common::require_admin_permission; @@ -40,6 +43,8 @@ enum CreatePasswordCredentialResponse { Created(Json), #[oai(status = 404)] NotFound, + #[oai(status = 422)] + PolicyViolation(Json>), } pub struct ListApi; @@ -87,6 +92,15 @@ impl ListApi { let db = ctx.services().db.lock().await; + let parameters = Parameters::Entity::get(&*db).await?; + let policy = parameters.password_policy(); + let violations = validate_password(body.password.expose_secret(), &policy); + if !violations.is_empty() { + return Ok(CreatePasswordCredentialResponse::PolicyViolation(Json( + violations, + ))); + } + let object = PasswordCredential::ActiveModel { id: Set(Uuid::new_v4()), user_id: Set(*user_id), diff --git a/warpgate-common/src/helpers/mod.rs b/warpgate-common/src/helpers/mod.rs index 2dc7fc312..5e05662ba 100644 --- a/warpgate-common/src/helpers/mod.rs +++ b/warpgate-common/src/helpers/mod.rs @@ -5,6 +5,7 @@ pub mod locks; pub mod logging; pub mod net; pub mod otp; +pub mod password_policy; pub mod rng; pub mod serde_base64; pub mod serde_base64_secret; diff --git a/warpgate-common/src/helpers/password_policy.rs b/warpgate-common/src/helpers/password_policy.rs new file mode 100644 index 000000000..2e6378ed9 --- /dev/null +++ b/warpgate-common/src/helpers/password_policy.rs @@ -0,0 +1,117 @@ +use poem_openapi::{Enum, Object}; +use serde::{Deserialize, Serialize}; + +/// Rules that a password must satisfy. +#[derive(Debug, Clone, Default, Serialize, Deserialize, Object, PartialEq, Eq)] +pub struct PasswordPolicy { + /// Minimum number of characters (0 = no requirement). + pub min_length: u32, + pub require_uppercase: bool, + pub require_lowercase: bool, + pub require_digits: bool, + pub require_special: bool, +} + +impl PasswordPolicy { + pub fn is_empty(&self) -> bool { + self.min_length == 0 + && !self.require_uppercase + && !self.require_lowercase + && !self.require_digits + && !self.require_special + } +} + +/// A single rule that was violated. +#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize, Enum)] +pub enum PasswordPolicyViolation { + TooShort, + MissingUppercase, + MissingLowercase, + MissingDigit, + MissingSpecial, +} + +/// Returns a list of violated rules. Empty list means the password is accepted. +pub fn validate_password(password: &str, policy: &PasswordPolicy) -> Vec { + let mut violations = Vec::new(); + + if policy.min_length > 0 && (password.chars().count() as u32) < policy.min_length { + violations.push(PasswordPolicyViolation::TooShort); + } + if policy.require_uppercase && !password.chars().any(|c| c.is_uppercase()) { + violations.push(PasswordPolicyViolation::MissingUppercase); + } + if policy.require_lowercase && !password.chars().any(|c| c.is_lowercase()) { + violations.push(PasswordPolicyViolation::MissingLowercase); + } + if policy.require_digits && !password.chars().any(|c| c.is_ascii_digit()) { + violations.push(PasswordPolicyViolation::MissingDigit); + } + if policy.require_special + && !password + .chars() + .any(|c| !c.is_alphanumeric() && c.is_ascii()) + { + violations.push(PasswordPolicyViolation::MissingSpecial); + } + + violations +} + +#[cfg(test)] +mod tests { + use super::*; + + fn policy() -> PasswordPolicy { + PasswordPolicy { + min_length: 8, + require_uppercase: true, + require_lowercase: true, + require_digits: true, + require_special: true, + } + } + + #[test] + fn accepts_strong_password() { + assert!(validate_password("Str0ng!Pass", &policy()).is_empty()); + } + + #[test] + fn rejects_too_short() { + let v = validate_password("Ab1!", &policy()); + assert!(v.contains(&PasswordPolicyViolation::TooShort)); + } + + #[test] + fn rejects_missing_uppercase() { + let v = validate_password("str0ng!pass", &policy()); + assert!(v.contains(&PasswordPolicyViolation::MissingUppercase)); + } + + #[test] + fn rejects_missing_lowercase() { + let v = validate_password("STR0NG!PASS", &policy()); + assert!(v.contains(&PasswordPolicyViolation::MissingLowercase)); + } + + #[test] + fn rejects_missing_digit() { + let v = validate_password("Strong!Pass", &policy()); + assert!(v.contains(&PasswordPolicyViolation::MissingDigit)); + } + + #[test] + fn rejects_missing_special() { + let v = validate_password("Str0ngPass", &policy()); + assert!(v.contains(&PasswordPolicyViolation::MissingSpecial)); + } + + #[test] + fn empty_policy_accepts_anything() { + let p = PasswordPolicy::default(); + assert!(validate_password("a", &p).is_empty()); + assert!(validate_password("", &p).is_empty()); + } +} diff --git a/warpgate-common/src/lib.rs b/warpgate-common/src/lib.rs index c8e3a2378..d49f1321b 100644 --- a/warpgate-common/src/lib.rs +++ b/warpgate-common/src/lib.rs @@ -13,5 +13,6 @@ pub mod version; pub use config::*; pub use error::WarpgateError; +pub use helpers::password_policy::{PasswordPolicy, PasswordPolicyViolation, validate_password}; pub use state::GlobalParams; pub use types::*; diff --git a/warpgate-db-entities/src/Parameters.rs b/warpgate-db-entities/src/Parameters.rs index 1d8db57cd..0ebcc3a86 100644 --- a/warpgate-db-entities/src/Parameters.rs +++ b/warpgate-db-entities/src/Parameters.rs @@ -3,6 +3,7 @@ use sea_orm::Set; use sea_orm::entity::prelude::*; use serde::Serialize; use uuid::Uuid; +use warpgate_common::PasswordPolicy; #[derive(Debug, PartialEq, Eq, Serialize, Clone, Copy, Enum, EnumIter, DeriveActiveEnum)] #[sea_orm(rs_type = "String", db_type = "String(StringLen::N(32))")] @@ -36,6 +37,11 @@ pub struct Model { pub ticket_request_show_all_targets: bool, pub target_click_action: TargetClickAction, pub show_session_menu: bool, + pub password_policy_min_length: i32, + pub password_policy_require_uppercase: bool, + pub password_policy_require_lowercase: bool, + pub password_policy_require_digits: bool, + pub password_policy_require_special: bool, pub max_api_token_duration_seconds: Option, pub record_scp: bool, } @@ -45,6 +51,18 @@ impl ActiveModelBehavior for ActiveModel {} #[derive(Copy, Clone, Debug, EnumIter, DeriveRelation)] pub enum Relation {} +impl Model { + pub fn password_policy(&self) -> PasswordPolicy { + PasswordPolicy { + min_length: self.password_policy_min_length.max(0) as u32, + require_uppercase: self.password_policy_require_uppercase, + require_lowercase: self.password_policy_require_lowercase, + require_digits: self.password_policy_require_digits, + require_special: self.password_policy_require_special, + } + } +} + impl Entity { pub async fn get(db: &DatabaseConnection) -> Result { match Self::find().one(db).await? { @@ -68,6 +86,11 @@ impl Entity { ticket_request_show_all_targets: Set(false), target_click_action: Set(TargetClickAction::Connect), show_session_menu: Set(true), + password_policy_min_length: Set(0), + password_policy_require_uppercase: Set(false), + password_policy_require_lowercase: Set(false), + password_policy_require_digits: Set(false), + password_policy_require_special: Set(false), max_api_token_duration_seconds: Set(None), record_scp: Set(true), } diff --git a/warpgate-db-migrations/src/lib.rs b/warpgate-db-migrations/src/lib.rs index 590834229..78f9a5d84 100644 --- a/warpgate-db-migrations/src/lib.rs +++ b/warpgate-db-migrations/src/lib.rs @@ -51,6 +51,7 @@ mod m00046_max_api_token_duration; mod m00047_record_scp; mod m00048_target_click_action; mod m00049_text_columns; +mod m00050_password_policy; pub(crate) mod helpers; @@ -109,6 +110,7 @@ impl MigratorTrait for Migrator { Box::new(m00047_record_scp::Migration), Box::new(m00048_target_click_action::Migration), Box::new(m00049_text_columns::Migration), + Box::new(m00050_password_policy::Migration), ] } } diff --git a/warpgate-db-migrations/src/m00050_password_policy.rs b/warpgate-db-migrations/src/m00050_password_policy.rs new file mode 100644 index 000000000..a73638383 --- /dev/null +++ b/warpgate-db-migrations/src/m00050_password_policy.rs @@ -0,0 +1,100 @@ +use sea_orm_migration::prelude::*; + +use crate::m00010_parameters::parameters; + +#[derive(DeriveMigrationName)] +pub struct Migration; + +#[async_trait::async_trait] +impl MigrationTrait for Migration { + async fn up(&self, manager: &SchemaManager) -> Result<(), DbErr> { + manager + .alter_table( + Table::alter() + .table(parameters::Entity) + .add_column( + ColumnDef::new(Alias::new("password_policy_min_length")) + .integer() + .not_null() + .default(0), + ) + .to_owned(), + ) + .await?; + + manager + .alter_table( + Table::alter() + .table(parameters::Entity) + .add_column( + ColumnDef::new(Alias::new("password_policy_require_uppercase")) + .boolean() + .not_null() + .default(false), + ) + .to_owned(), + ) + .await?; + + manager + .alter_table( + Table::alter() + .table(parameters::Entity) + .add_column( + ColumnDef::new(Alias::new("password_policy_require_lowercase")) + .boolean() + .not_null() + .default(false), + ) + .to_owned(), + ) + .await?; + + manager + .alter_table( + Table::alter() + .table(parameters::Entity) + .add_column( + ColumnDef::new(Alias::new("password_policy_require_digits")) + .boolean() + .not_null() + .default(false), + ) + .to_owned(), + ) + .await?; + + manager + .alter_table( + Table::alter() + .table(parameters::Entity) + .add_column( + ColumnDef::new(Alias::new("password_policy_require_special")) + .boolean() + .not_null() + .default(false), + ) + .to_owned(), + ) + .await?; + + Ok(()) + } + + async fn down(&self, manager: &SchemaManager) -> Result<(), DbErr> { + manager + .alter_table( + Table::alter() + .table(parameters::Entity) + .drop_column(Alias::new("password_policy_min_length")) + .drop_column(Alias::new("password_policy_require_uppercase")) + .drop_column(Alias::new("password_policy_require_lowercase")) + .drop_column(Alias::new("password_policy_require_digits")) + .drop_column(Alias::new("password_policy_require_special")) + .to_owned(), + ) + .await?; + + Ok(()) + } +} diff --git a/warpgate-protocol-http/src/api/credentials.rs b/warpgate-protocol-http/src/api/credentials.rs index ba6195455..f7b8a264e 100644 --- a/warpgate-protocol-http/src/api/credentials.rs +++ b/warpgate-protocol-http/src/api/credentials.rs @@ -7,7 +7,10 @@ use poem_openapi::{ApiResponse, Enum, Object, OpenApi}; use sea_orm::{ActiveModelTrait, ColumnTrait, EntityTrait, ModelTrait, QueryFilter, Set}; use time::OffsetDateTime; use uuid::Uuid; -use warpgate_common::{User, UserPasswordCredential, UserRequireCredentialsPolicy, WarpgateError}; +use warpgate_common::{ + PasswordPolicy, PasswordPolicyViolation, User, UserPasswordCredential, + UserRequireCredentialsPolicy, WarpgateError, validate_password, +}; use warpgate_common_http::auth::{AuthenticatedRequestContext, UnauthenticatedRequestContext}; use warpgate_core::logging::{AuditEvent, CredentialChangedVia}; use warpgate_db_entities::{ @@ -55,6 +58,8 @@ enum ChangePasswordResponse { Done(Json), #[oai(status = 401)] Unauthorized, + #[oai(status = 422)] + PolicyViolation(Json>), } #[derive(Object)] @@ -66,6 +71,7 @@ pub struct CredentialsState { sso: Vec, credential_policy: UserRequireCredentialsPolicy, ldap_linked: bool, + password_policy: PasswordPolicy, } #[derive(ApiResponse)] @@ -278,6 +284,8 @@ impl Api { .all(&*db) .await?; + let parameters = Parameters::Entity::get(&*db).await?; + Ok(CredentialsStateResponse::Ok(Json(CredentialsState { password: match password_creds.len() { 0 => PasswordState::Unset, @@ -290,6 +298,7 @@ impl Api { sso: sso_creds.into_iter().map(Into::into).collect(), credential_policy: user_cfg.credential_policy.unwrap_or_default(), ldap_linked: user.ldap_server_id.is_some(), + password_policy: parameters.password_policy(), }))) } @@ -312,6 +321,13 @@ impl Api { return Ok(ChangePasswordResponse::Unauthorized); }; + let parameters = Parameters::Entity::get(&*db).await?; + let policy = parameters.password_policy(); + let violations = validate_password(&body.password, &policy); + if !violations.is_empty() { + return Ok(ChangePasswordResponse::PolicyViolation(Json(violations))); + } + entities::PasswordCredential::Entity::delete_many() .filter(entities::PasswordCredential::Column::UserId.eq(user.id)) .exec(&*db) diff --git a/warpgate-web/src/admin/CreatePasswordModal.svelte b/warpgate-web/src/admin/CreatePasswordModal.svelte index 9eec1fef3..982691fb8 100644 --- a/warpgate-web/src/admin/CreatePasswordModal.svelte +++ b/warpgate-web/src/admin/CreatePasswordModal.svelte @@ -1,4 +1,6 @@

Connection

-
+ {#if sshTargets.length} +
+ + + +
+ {/if} +
hostKeyCheckInvalidated = true} />
-
+
hostKeyCheckInvalidated = true} />
-

Authentication

- {#if $adminPermissions.targetsEdit}
{#if !hostKeyCheckInvalidated} @@ -51,6 +67,8 @@
{/if} +

Authentication

+ Date: Sun, 7 Jun 2026 02:05:43 +0200 Subject: [PATCH 145/556] add rjourdan04 as a contributor for code (#2014) Adds @rjourdan04 as a contributor for code. This was requested by Eugeny [in this comment](https://github.com/warp-tech/warpgate/pull/1885#issuecomment-4639706044) --------- Co-authored-by: allcontributors[bot] <46447321+allcontributors[bot]@users.noreply.github.com> --- .all-contributorsrc | 9 +++++++++ README.md | 3 +++ 2 files changed, 12 insertions(+) diff --git a/.all-contributorsrc b/.all-contributorsrc index 22a96881c..4117a7491 100644 --- a/.all-contributorsrc +++ b/.all-contributorsrc @@ -198,6 +198,15 @@ "contributions": [ "code" ] + }, + { + "login": "rjourdan04", + "name": "rjourdan04", + "avatar_url": "https://avatars.githubusercontent.com/u/181946490?v=4", + "profile": "https://github.com/rjourdan04", + "contributions": [ + "code" + ] } ], "contributorsPerLine": 7, diff --git a/README.md b/README.md index 09db21302..5238480f6 100644 --- a/README.md +++ b/README.md @@ -169,6 +169,9 @@ Thanks goes to these wonderful people ([emoji key](https://allcontributors.org/d Lukas Klepper
Lukas Klepper

💻 kamilkrzeminski
kamilkrzeminski

💻 + + rjourdan04
rjourdan04

💻 + From df6432f17f0a32a75a496062a031d8f73c79f161 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Sun, 7 Jun 2026 09:50:34 +0200 Subject: [PATCH 146/556] Bump md5 from 0.7.0 to 0.8.0 (#2011) Signed-off-by: dependabot[bot] --- Cargo.lock | 2 +- warpgate-protocol-kubernetes/Cargo.toml | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index 6542113e1..c8ac3677f 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -7915,7 +7915,7 @@ dependencies = [ "dashmap", "futures", "http 1.4.0", - "md5 0.7.0", + "md5 0.8.0", "poem", "poem-openapi", "regex", diff --git a/warpgate-protocol-kubernetes/Cargo.toml b/warpgate-protocol-kubernetes/Cargo.toml index 97fa069a2..e4691e4c2 100644 --- a/warpgate-protocol-kubernetes/Cargo.toml +++ b/warpgate-protocol-kubernetes/Cargo.toml @@ -13,7 +13,7 @@ bytes.workspace = true dashmap = { version = "6.0", default-features = false } futures.workspace = true http = { version = "1.0", default-features = false } -md5 = { version = "0.7", default-features = false } +md5 = { version = "0.8", default-features = false } poem.workspace = true poem-openapi.workspace = true regex.workspace = true From c3147baf6d15e857922225c5bf727a24e9139662 Mon Sep 17 00:00:00 2001 From: Eugene Date: Sun, 7 Jun 2026 10:26:20 +0200 Subject: [PATCH 147/556] fixed #2001 - incorrect version tag baked into the app --- warpgate-common/src/version.rs | 2 +- warpgate-web/src/admin/lib/openapi-schema.json | 2 +- warpgate-web/src/gateway/lib/openapi-schema.json | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/warpgate-common/src/version.rs b/warpgate-common/src/version.rs index 0e7985aac..0b7af9774 100644 --- a/warpgate-common/src/version.rs +++ b/warpgate-common/src/version.rs @@ -2,7 +2,7 @@ use git_version::git_version; pub const fn warpgate_version() -> &'static str { git_version!( - args = ["--tags", "--always", "--dirty=-modified"], + args = ["--tags", "--always", "--dirty=-modified", "--match", "v[0-9]*"], fallback = "unknown" ) } diff --git a/warpgate-web/src/admin/lib/openapi-schema.json b/warpgate-web/src/admin/lib/openapi-schema.json index 7aed6b837..babdcc508 100644 --- a/warpgate-web/src/admin/lib/openapi-schema.json +++ b/warpgate-web/src/admin/lib/openapi-schema.json @@ -2,7 +2,7 @@ "openapi": "3.0.0", "info": { "title": "Warpgate Web Admin", - "version": "chart-v0.0.5-23-g1094e944-modified" + "version": "v0.24.0-23-gdf6432f1-modified" }, "servers": [ { diff --git a/warpgate-web/src/gateway/lib/openapi-schema.json b/warpgate-web/src/gateway/lib/openapi-schema.json index 302e0aeac..8a5adb1f8 100644 --- a/warpgate-web/src/gateway/lib/openapi-schema.json +++ b/warpgate-web/src/gateway/lib/openapi-schema.json @@ -2,7 +2,7 @@ "openapi": "3.0.0", "info": { "title": "Warpgate HTTP proxy", - "version": "chart-v0.0.5-23-g1094e944-modified" + "version": "v0.24.0-23-gdf6432f1-modified" }, "servers": [ { From d11a994695a3d6cf4df05360bb4053e7d666dd81 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Sun, 7 Jun 2026 11:19:48 +0200 Subject: [PATCH 148/556] Bump jsonwebtoken from 9.3.1 to 10.3.0 (#2012) Co-authored-by: Eugene Signed-off-by: dependabot[bot] --- Cargo.lock | 8 +++++--- warpgate-sso/Cargo.toml | 2 +- 2 files changed, 6 insertions(+), 4 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index c8ac3677f..c2cfdc5e8 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -3296,16 +3296,18 @@ dependencies = [ [[package]] name = "jsonwebtoken" -version = "9.3.1" +version = "10.3.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5a87cc7a48537badeae96744432de36f4be2b4a34a05a5ef32e9dd8a1c169dde" +checksum = "0529410abe238729a60b108898784df8984c87f6054c9c4fcacc47e4803c1ce1" dependencies = [ + "aws-lc-rs", "base64 0.22.1", + "getrandom 0.2.17", "js-sys", "pem", - "ring", "serde", "serde_json", + "signature 2.2.0", "simple_asn1", ] diff --git a/warpgate-sso/Cargo.toml b/warpgate-sso/Cargo.toml index 78856be83..8cf5a348e 100644 --- a/warpgate-sso/Cargo.toml +++ b/warpgate-sso/Cargo.toml @@ -19,7 +19,7 @@ reqwest_12.workspace = true reqwest_12.features = ["json"] serde.workspace = true serde_json.workspace = true -jsonwebtoken = { version = "9", default-features = false, features = ["use_pem"] } +jsonwebtoken = { version = "10", default-features = false, features = ["use_pem", "aws_lc_rs"] } data-encoding.workspace = true futures.workspace = true schemars.workspace = true From b79f2e70dced5840981e5d9bf2cabd16afad1be8 Mon Sep 17 00:00:00 2001 From: Eugene Date: Sun, 7 Jun 2026 12:37:57 +0200 Subject: [PATCH 149/556] New connection animation (#2017) --- tests/test_api.py | 140 ++++++----- tests/test_ssh_proto.py | 3 +- warpgate-admin/src/api/ssh_connection_test.rs | 40 +-- warpgate-common/src/helpers/hash.rs | 13 +- warpgate-protocol-postgres/src/session.rs | 12 +- warpgate-protocol-ssh/src/client/mod.rs | 229 +++++++++--------- .../src/server/service_output.rs | 220 +++++++++++++++-- warpgate-protocol-ssh/src/server/session.rs | 167 +++++++------ .../src/server/target_menu.rs | 6 +- warpgate-web-ssh/src/manager.rs | 9 +- .../config/targets/ssh/KeyChecker.svelte | 7 +- .../src/admin/lib/openapi-schema.json | 23 +- .../src/gateway/lib/openapi-schema.json | 2 +- 13 files changed, 528 insertions(+), 343 deletions(-) diff --git a/tests/test_api.py b/tests/test_api.py index 4be6b9ba6..29c2b3896 100644 --- a/tests/test_api.py +++ b/tests/test_api.py @@ -705,7 +705,7 @@ def make_limited_admin_role_payload(**overrides): id="check_ssh_host_key", permission="targets_edit", call=lambda api, r: api.check_ssh_host_key_with_http_info( - sdk.CheckSshHostKeyRequest(host="127.0.0.1", port=22), + sdk.CheckSshHostKeyRequest(target_id=r["target_id"]), ), expected_statuses={200}, ), @@ -877,27 +877,34 @@ def admin_client(pg_wg: WarpgateProcess): yield api -def test_all_openapi_admin_operations_permission_enforcement( - pg_wg: WarpgateProcess, admin_client: sdk.DefaultApi -): - _verify_all_openapi_ops_are_covered() - +@pytest.fixture(scope="session") +def _session_admin_client(pg_wg: WarpgateProcess): url = f"https://localhost:{pg_wg.http_port}" + with new_admin_client(url) as api: + yield api + +@pytest.fixture(scope="session") +def api_test_resources( + pg_wg: WarpgateProcess, _session_admin_client: sdk.DefaultApi +) -> Dict[str, object]: + _verify_all_openapi_ops_are_covered() + + ac = _session_admin_client resources: Dict[str, object] = {} - resources["role_id"] = admin_client.create_role( + resources["role_id"] = ac.create_role( sdk.RoleDataRequest(name=f"role-{uuid4()}") ).id resources["admin_role_id"] = _create_admin_role( - admin_client, + ac, make_limited_admin_role_payload(name=f"admin-role-{uuid4()}"), ).id - resources["target_group_id"] = admin_client.create_target_group( + resources["target_group_id"] = ac.create_target_group( sdk.TargetGroupDataRequest( name=f"group-{uuid4()}", description="", color=sdk.BootstrapThemeColor.INFO ) ).id - user = admin_client.create_user(sdk.CreateUserRequest(username=f"user-{uuid4()}")) + user = ac.create_user(sdk.CreateUserRequest(username=f"user-{uuid4()}")) resources["user_id"] = user.id resources["username"] = user.username @@ -907,7 +914,7 @@ def test_all_openapi_admin_operations_permission_enforcement( resources["ssh_known_host_id"] = str(uuid4()) resources["ticket_request_id"] = str(uuid4()) - target = admin_client.create_target( + target = ac.create_target( sdk.TargetDataRequest( name=f"target-{uuid4()}", options=sdk.TargetOptions( @@ -926,23 +933,23 @@ def test_all_openapi_admin_operations_permission_enforcement( resources["target_id"] = target.id resources["target_name"] = target.name - ticket = admin_client.create_ticket( + ticket = ac.create_ticket( sdk.CreateTicketRequest( username=resources["username"], target_name=resources["target_name"] ) ) resources["ticket_id"] = ticket.ticket.id - pw = admin_client.create_password_credential( + pw = ac.create_password_credential( resources["user_id"], sdk.NewPasswordCredential(password="123") ) resources["password_id"] = pw.id - sso = admin_client.create_sso_credential( + sso = ac.create_sso_credential( resources["user_id"], sdk.NewSsoCredential(email="test@example.com", provider="test"), ) resources["sso_id"] = sso.id - public_key = admin_client.create_public_key_credential( + public_key = ac.create_public_key_credential( resources["user_id"], sdk.NewPublicKeyCredential( label="key", @@ -950,11 +957,11 @@ def test_all_openapi_admin_operations_permission_enforcement( ), ) resources["public_key_id"] = public_key.id - otp = admin_client.create_otp_credential( + otp = ac.create_otp_credential( resources["user_id"], sdk.NewOtpCredential(name="otp-1", secret_key=[1, 2, 3]) ) resources["otp_id"] = otp.id - cert = admin_client.issue_certificate_credential( + cert = ac.issue_certificate_credential( resources["user_id"], sdk.IssueCertificateCredentialRequest( label="test", @@ -962,7 +969,7 @@ def test_all_openapi_admin_operations_permission_enforcement( ), ) resources["certificate_id"] = cert.credential.id - ldap = admin_client.create_ldap_server( + ldap = ac.create_ldap_server( sdk.CreateLdapServerRequest( name=f"ldap-{uuid4()}", host="127.0.0.1", @@ -972,50 +979,57 @@ def test_all_openapi_admin_operations_permission_enforcement( ) resources["ldap_server_id"] = ldap.id - for case in ADMIN_API_TEST_CASES: - print(f"Testing {case.id} with permission {case.permission}") - # Positive case: role has required permission (or any admin if None). - allow_payload = make_limited_admin_role_payload( - **({case.permission: True} if case.permission else {}) + return resources + + +@pytest.mark.parametrize( + "case", + ADMIN_API_TEST_CASES, + ids=[c.id for c in ADMIN_API_TEST_CASES], +) +def test_admin_api_permission_enforcement( + pg_wg: WarpgateProcess, + admin_client: sdk.DefaultApi, + api_test_resources: Dict[str, object], + case: AdminApiTestCase, +): + url = f"https://localhost:{pg_wg.http_port}" + + allow_payload = make_limited_admin_role_payload( + **({case.permission: True} if case.permission else {}) + ) + allowed_role = _create_admin_role(admin_client, allow_payload) + allowed_user = _create_user_with_role(admin_client, allowed_role.id) + token = _create_user_api_token(url, allowed_user.username, "123") + with new_admin_client(url, token) as allowed_api: + try: + response = case.call(allowed_api, api_test_resources) + (status, body) = response.status_code, response.data + except sdk.ApiException as e: + (status, body) = e.status, e.body + assert status in case.expected_statuses, ( + f"{case.id} expected {case.expected_statuses} but got {status}: {body}" ) - allowed_role = _create_admin_role(admin_client, allow_payload) - allowed_user = _create_user_with_role(admin_client, allowed_role.id) - token = _create_user_api_token(url, allowed_user.username, "123") - with new_admin_client(url, token) as allowed_api: - print("Trying positive case") - try: - response = case.call(allowed_api, resources) - (status, body) = response.status_code, response.data - except sdk.ApiException as e: - (status, body) = e.status, e.body - assert status in case.expected_statuses, ( - f"{case.id} expected {case.expected_statuses} but got {status}: {body}" - ) - # Negative case: permission missing should be rejected. - if case.permission: - denied_role = _create_admin_role( - admin_client, - { - k: not v if isinstance(v, bool) else v - for k, v in allow_payload.items() - }, - ) - denied_user = _create_user_with_role(admin_client, denied_role.id) - else: - denied_user = _create_user_with_role(admin_client, None) - - denied_token = _create_user_api_token(url, denied_user.username, "123") - - with new_admin_client( - f"https://localhost:{pg_wg.http_port}", denied_token - ) as denied_api: - print("Trying negative case") - try: - response = case.call(denied_api, resources) - (status, body) = response.status_code, response.data - except sdk.ApiException as e: - (status, body) = e.status, e.body - assert status in {401, 403}, ( - f"{case.id} should be forbidden without {case.permission}, got {status}: {body}" - ) + if case.permission: + denied_role = _create_admin_role( + admin_client, + { + k: not v if isinstance(v, bool) else v + for k, v in allow_payload.items() + }, + ) + denied_user = _create_user_with_role(admin_client, denied_role.id) + else: + denied_user = _create_user_with_role(admin_client, None) + + denied_token = _create_user_api_token(url, denied_user.username, "123") + with new_admin_client(url, denied_token) as denied_api: + try: + response = case.call(denied_api, api_test_resources) + (status, body) = response.status_code, response.data + except sdk.ApiException as e: + (status, body) = e.status, e.body + assert status in {401, 403}, ( + f"{case.id} should be forbidden without {case.permission}, got {status}: {body}" + ) diff --git a/tests/test_ssh_proto.py b/tests/test_ssh_proto.py index a1b3f2b6b..b53e0d89e 100644 --- a/tests/test_ssh_proto.py +++ b/tests/test_ssh_proto.py @@ -122,8 +122,7 @@ def test_pty( ) output = ssh_client.communicate(timeout=timeout)[0] - assert b"Warpgate" in output - assert b"Selected target:" in output + assert ssh_target.name.encode() in output assert b"hello\r\n" in output def test_signals( diff --git a/warpgate-admin/src/api/ssh_connection_test.rs b/warpgate-admin/src/api/ssh_connection_test.rs index fb9cdcf1c..2ae4c25dc 100644 --- a/warpgate-admin/src/api/ssh_connection_test.rs +++ b/warpgate-admin/src/api/ssh_connection_test.rs @@ -3,11 +3,9 @@ use poem_openapi::payload::{Json, PlainText}; use poem_openapi::{ApiResponse, Object, OpenApi}; use russh::keys::PublicKeyBase64; use uuid::Uuid; -use warpgate_common::{ - AdminPermission, SSHTargetAuth, SshTargetPasswordAuth, TargetSSHOptions, WarpgateError, -}; +use warpgate_common::{AdminPermission, WarpgateError}; use warpgate_common_http::AuthenticatedRequestContext; -use warpgate_protocol_ssh::{RCCommand, RCEvent, RemoteClient}; +use warpgate_protocol_ssh::{RCCommand, RCEvent, RemoteClient, resolve_ssh_chain}; use super::AnySecurityScheme; use crate::api::common::require_admin_permission; @@ -16,12 +14,7 @@ pub struct Api; #[derive(Object)] struct CheckSshHostKeyRequest { - host: String, - port: u16, - username: Option, - allow_insecure_algos: Option, - auth: Option, - jump_host: Option, + target_id: Uuid, } #[derive(Object)] @@ -53,23 +46,16 @@ impl Api { ) -> Result { require_admin_permission(&ctx, Some(AdminPermission::TargetsEdit)).await?; - let mut handles = RemoteClient::create(Uuid::new_v4(), ctx.services().clone())?; + let ssh_chain = resolve_ssh_chain(ctx.services(), body.target_id, ctx.auth.username()) + .await? + .into_iter() + .map(|x| x.ssh_options) + .collect::>(); - let _ = handles.command_tx.send(( - RCCommand::Connect(TargetSSHOptions { - host: body.host.clone(), - port: body.port, - username: body.username.clone().unwrap_or_default(), - allow_insecure_algos: body.allow_insecure_algos, - auth: body.auth.clone().unwrap_or_else(|| { - SSHTargetAuth::Password(SshTargetPasswordAuth { - password: String::new().into(), - }) - }), - jump_host: body.jump_host.clone(), - }), - None, - )); + let mut handles = RemoteClient::create(Uuid::new_v4(), ctx.services().clone())?; + let _ = handles + .command_tx + .send((RCCommand::Connect(ssh_chain), None)); let fut = async move { let key = loop { @@ -78,7 +64,7 @@ impl Api { Some(RCEvent::HostKeyUnknown(key, reply)) => { let _ = reply.send(true); break key; - }, + } Some(RCEvent::ConnectionError(err)) => return Err(anyhow::Error::from(err)), Some(RCEvent::Error(err)) => return Err(err), None => anyhow::bail!("Failed to connect to target"), diff --git a/warpgate-common/src/helpers/hash.rs b/warpgate-common/src/helpers/hash.rs index b959e551a..0291eba43 100644 --- a/warpgate-common/src/helpers/hash.rs +++ b/warpgate-common/src/helpers/hash.rs @@ -9,7 +9,18 @@ use crate::Secret; pub fn hash_password(password: &str) -> String { let salt = SaltString::generate(&mut OsRng); - let argon2 = Argon2::default(); + let argon2 = if std::env::var("WARPGATE_UNDER_TEST") + .unwrap_or_default() + .is_empty() + { + Argon2::default() + } else { + Argon2::new( + argon2::Algorithm::Argon2id, + argon2::Version::V0x13, + argon2::Params::new(1000, 1, 1, None).unwrap(), + ) + }; // Only panics for invalid hash parameters #[allow(clippy::unwrap_used)] argon2 diff --git a/warpgate-protocol-postgres/src/session.rs b/warpgate-protocol-postgres/src/session.rs index 50d1aef69..d24f244b0 100644 --- a/warpgate-protocol-postgres/src/session.rs +++ b/warpgate-protocol-postgres/src/session.rs @@ -543,18 +543,18 @@ impl PostgresSession { && let Some(upgraded_key) = self .cancel_key_downgrade_map .remove(&cancel_request.secret_key) - { - cancel_request.secret_key = upgraded_key; - } + { + cancel_request.secret_key = upgraded_key; + } if client.protocol_version() == ProtocolVersion::PROTOCOL3_0 { // Transform cancel keys to 3.0 format if needed if let SecretKey::Bytes(_) = cancel_request.secret_key && let Some(downgraded_key) = self .cancel_key_upgrade_map .remove(&cancel_request.secret_key) - { - cancel_request.secret_key = downgraded_key; - } + { + cancel_request.secret_key = downgraded_key; + } } } msg diff --git a/warpgate-protocol-ssh/src/client/mod.rs b/warpgate-protocol-ssh/src/client/mod.rs index d57e01da7..27ffdde71 100644 --- a/warpgate-protocol-ssh/src/client/mod.rs +++ b/warpgate-protocol-ssh/src/client/mod.rs @@ -6,7 +6,6 @@ use std::borrow::Cow; use std::collections::HashMap; use std::io; use std::net::ToSocketAddrs; -use std::pin::Pin; use std::sync::Arc; use anyhow::Result; @@ -14,7 +13,7 @@ use bytes::Bytes; use channel_direct_tcpip::DirectTCPIPChannel; use channel_session::SessionChannel; pub use error::SshClientError; -use futures::{FutureExt, pin_mut}; +use futures::pin_mut; use handler::ClientHandler; use russh::client::{AuthResult, Handle, KeyboardInteractiveAuthResponse}; use russh::keys::{PrivateKeyWithHashAlg, PublicKey}; @@ -28,7 +27,7 @@ use tokio::task::JoinHandle; use tracing::*; use uuid::Uuid; use warpgate_aws::AwsError; -use warpgate_common::{SSHTargetAuth, SessionId, TargetOptions, TargetSSHOptions}; +use warpgate_common::{SSHTargetAuth, SessionId, TargetOptions, TargetSSHOptions, WarpgateError}; use warpgate_core::{ConfigProvider, Services}; use self::handler::ClientHandlerEvent; @@ -74,6 +73,51 @@ pub enum ConnectionError { JumpHostTargetNotFound, } +pub struct ResolvedSshChainHost { + pub name: String, + pub ssh_options: TargetSSHOptions, +} + +/// Resolve the full ordered SSH jump chain for a target +/// `logged_in_username` is used to substitute empty dynamic usernames +/// in targets' configs +pub async fn resolve_ssh_chain( + services: &Services, + target_id: Uuid, + logged_in_username: Option<&String>, +) -> Result, WarpgateError> { + let mut jumps = vec![]; + let mut current_jump_id = Some(target_id); + let targets = services.config_provider.lock().await.list_targets().await?; + while let Some(id) = current_jump_id { + let Some(t) = targets.iter().find(|t| t.id == id) else { + break; + }; + let name = t.name.clone(); + match &t.options { + TargetOptions::Ssh(opts) => { + let mut opts = opts.clone(); + current_jump_id = opts.jump_host; + + // Forward username from the authenticated user to the target, if target has no username + if let Some(logged_in_username) = logged_in_username + && opts.username.is_empty() + { + opts.username = logged_in_username.clone(); + } + + jumps.push(ResolvedSshChainHost { + name, + ssh_options: opts.clone(), + }); + } + _ => break, + } + } + jumps.reverse(); + Ok(jumps) +} + #[derive(Debug)] pub enum RCEvent { State(RCState), @@ -97,6 +141,7 @@ pub enum RCEvent { ext: u32, }, ConnectionError(ConnectionError), + HopConnected, // ForwardedTCPIP(Uuid, DirectTCPIPParams), Done, HostKeyReceived(PublicKey), @@ -111,7 +156,7 @@ pub type RCCommandReply = oneshot::Sender>; #[derive(Clone, Debug)] pub enum RCCommand { - Connect(TargetSSHOptions), + Connect(Vec), Channel(Uuid, ChannelOperation), ForwardTCPIP(String, u32), CancelTCPIPForward(String, u32), @@ -506,119 +551,75 @@ impl RemoteClient { Arc::new(config) } - async fn resolve_jump_target( - &self, - jump_host_id: Uuid, - ) -> Result { - let targets: Vec<_> = self - .services - .config_provider - .lock() - .await - .list_targets() - .await - .map_err(|_| ConnectionError::JumpHostTargetNotFound)?; - let target = targets - .into_iter() - .find(|t| t.id == jump_host_id) - .ok_or(ConnectionError::JumpHostTargetNotFound)?; - match target.options { - TargetOptions::Ssh(opts) => Ok(opts), - _ => Err(ConnectionError::JumpHostTargetNotFound), + /// Connect through a pre-resolved chain of SSH hops, each tunnelled through the previous. + /// `chain` must be non-empty; the first entry is connected directly, subsequent ones via + /// `channel_open_direct_tcpip` through the previous session. + async fn connect_chain( + &mut self, + chain: Vec, + ) -> Result<(Handle, UnboundedReceiver), ConnectionError> + { + let mut iter = chain.into_iter(); + let first = iter.next().ok_or(ConnectionError::Resolve)?; + + let config = self.build_ssh_config(&first).await; + let address_str = format!("{}:{}", first.host, first.port); + let address = address_str + .to_socket_addrs() + .map_err(ConnectionError::Io) + .and_then(|mut x| x.next().ok_or(ConnectionError::Resolve)) + .inspect_err(|e| error!(?e, address=%address_str, "Cannot resolve address"))?; + info!(?address, username = %first.username, "Connecting"); + let (event_tx, event_rx) = unbounded_channel(); + let handler = ClientHandler { + ssh_options: first.clone(), + event_tx, + services: self.services.clone(), + session_id: self.id, + }; + let fut = russh::client::connect(config, address, handler); + let (mut session, mut active_rx) = self + .wait_for_connection(&first, fut, event_rx, false) + .await?; + + for ssh_options in iter { + let _ = self.tx.send(RCEvent::HopConnected).await; + info!( + host = %ssh_options.host, + port = ssh_options.port, + "Opening direct-tcpip channel through jump host" + ); + let channel = session + .channel_open_direct_tcpip( + ssh_options.host.clone(), + ssh_options.port as u32, + "localhost".to_string(), + 0, + ) + .await + .map_err(ConnectionError::Ssh)?; + let stream = channel.into_stream(); + let config = self.build_ssh_config(&ssh_options).await; + let (event_tx, event_rx) = unbounded_channel(); + let handler = ClientHandler { + ssh_options: ssh_options.clone(), + event_tx, + services: self.services.clone(), + session_id: self.id, + }; + let fut = russh::client::connect_stream(config, stream, handler); + let (new_session, new_rx) = self + .wait_for_connection(&ssh_options, fut, event_rx, false) + .await?; + session = new_session; + active_rx = new_rx; } - } - /// Connect to target, recursively opening jump channels if needed - fn open_session<'a>( - &'a mut self, - ssh_options: TargetSSHOptions, - config: Arc, - ) -> Pin< - Box< - dyn Future< - Output = Result< - (Handle, UnboundedReceiver), - ConnectionError, - >, - > + Send - + 'a, - >, - > { - async move { - if let Some(jump_host_id) = ssh_options.jump_host { - let jump_ssh_options = self.resolve_jump_target(jump_host_id).await?; - let jump_config = self.build_ssh_config(&jump_ssh_options).await; - - info!( - host = %jump_ssh_options.host, - port = jump_ssh_options.port, - username = %jump_ssh_options.username, - "Connecting to jump host" - ); - let (jump_session, _) = self.open_session(jump_ssh_options, jump_config).await?; - - info!( - host = %ssh_options.host, - port = ssh_options.port, - "Opening direct-tcpip channel through jump host" - ); - let channel = jump_session - .channel_open_direct_tcpip( - ssh_options.host.clone(), - ssh_options.port as u32, - "localhost".to_string(), - 0, - ) - .await - .map_err(ConnectionError::Ssh)?; - - let stream = channel.into_stream(); - let (event_tx, event_rx) = unbounded_channel(); - let handler = ClientHandler { - ssh_options: ssh_options.clone(), - event_tx, - services: self.services.clone(), - session_id: self.id, - }; - let fut_connect = russh::client::connect_stream(config, stream, handler); - self.wait_for_connection(&ssh_options, fut_connect, event_rx, false) - .await - } else { - let address_str = format!("{}:{}", ssh_options.host, ssh_options.port); - let address = address_str - .to_socket_addrs() - .map_err(ConnectionError::Io) - .and_then(|mut x| x.next().ok_or(ConnectionError::Resolve)) - .inspect_err(|e| { - error!(?e, address=%address_str, "Cannot resolve address"); - })?; - - info!(?address, username = %ssh_options.username, "Connecting to target"); - let (event_tx, event_rx) = unbounded_channel(); - let handler = ClientHandler { - ssh_options: ssh_options.clone(), - event_tx, - services: self.services.clone(), - session_id: self.id, - }; - - let fut_connect = russh::client::connect(config, address, handler); - self.wait_for_connection(&ssh_options, fut_connect, event_rx, false) - .await - } - } - .boxed() + Ok((session, active_rx)) } - async fn connect(&mut self, ssh_options: TargetSSHOptions) -> Result<(), ConnectionError> { - info!( - host = %ssh_options.host, - port = ssh_options.port, - username = %ssh_options.username, - "Connecting" - ); - let config = self.build_ssh_config(&ssh_options).await; - let (session, mut event_rx) = self.open_session(ssh_options, config).await?; + async fn connect(&mut self, chain: Vec) -> Result<(), ConnectionError> { + let (session, mut event_rx) = self.connect_chain(chain).await?; self.session = Some(Arc::new(Mutex::new(session))); @@ -638,7 +639,7 @@ impl RemoteClient { .instrument(Span::current()), ); - return Ok(()); + Ok(()) } async fn wait_for_connection( diff --git a/warpgate-protocol-ssh/src/server/service_output.rs b/warpgate-protocol-ssh/src/server/service_output.rs index b2d28963a..0ce1c96c9 100644 --- a/warpgate-protocol-ssh/src/server/service_output.rs +++ b/warpgate-protocol-ssh/src/server/service_output.rs @@ -1,25 +1,154 @@ +use std::borrow::Cow; +use std::fmt::Display; use std::io::Write as _; use std::sync::Arc; -use std::sync::atomic::AtomicBool; +use std::sync::atomic::{AtomicBool, AtomicUsize, Ordering}; +use std::time::Duration; use bytes::Bytes; use termcolor::{Buffer, Color, ColorSpec, WriteColor as _}; -use tokio::sync::{broadcast, mpsc}; +use tokio::sync::{Mutex, broadcast, mpsc}; -pub const ERASE_PROGRESS_SPINNER: &str = "\r \r"; -pub const ERASE_PROGRESS_SPINNER_BUF: &[u8] = ERASE_PROGRESS_SPINNER.as_bytes(); +const SEG_LEN: usize = 5; +const ANIM_FRAME_DURATION: Duration = Duration::from_millis(100); -pub(super) fn ansi_paint(fg: Color, bg: Color, text: &str) -> String { +const CH_SEGMENT_ANIMATION: [char; 3] = ['┈', '─', '┈']; +const CH_SEGMENT_CONNECTED: char = '─'; +const CH_SEGMENT_NOT_CONNECTED: char = '─'; +const CH_TARGET_CONNECTED: char = '●'; +const CH_TARGET_NOT_CONNECTED: char = '○'; +const CURSOR_UP: &str = "\x1b[1A"; + +#[must_use] +pub(super) fn paint_fg(fg: Color, dimmed: bool, text: S) -> String { let mut buf = Buffer::ansi(); - let _ = buf.set_color(ColorSpec::new().set_fg(Some(fg)).set_bg(Some(bg))); + let _ = buf.set_color(ColorSpec::new().set_fg(Some(fg)).set_dimmed(dimmed)); let _ = write!(buf, "{text}"); let _ = buf.reset(); String::from_utf8_lossy(buf.as_slice()).to_string() } +#[derive(Clone)] +pub enum VisualConnectionChainItem { + Text(String), + Link { text: String, url: String }, +} + +impl VisualConnectionChainItem { + pub fn ansi<'a>(&'a self) -> Cow<'a, str> { + match self { + VisualConnectionChainItem::Text(s) => Cow::Borrowed(s), + VisualConnectionChainItem::Link { text, url } => { + Cow::Owned(format!("\x1b]8;;{url}\x1b\\{text}\x1b]8;;\x1b\\")) + } + } + } +} + +/// Describes the connection chain hosts and how many hops are fully connected. +pub struct VisualConnectionChainState { + /// Host display names + pub items: Vec, + /// Number of segments (between adjacent hosts) that are fully connected (green). + /// Starts at 1 because the you → warpgate link is always established. + pub connected_hops: usize, +} + +#[derive(Clone, Copy, PartialEq, Eq)] +enum SegmentState { + Connected, + Connecting, + Pending, +} + +#[must_use] +fn render_segment_line(state: SegmentState, tick: usize) -> String { + match state { + SegmentState::Connected => paint_fg( + Color::Green, + false, + CH_SEGMENT_CONNECTED.to_string().repeat(SEG_LEN), + ), + SegmentState::Connecting => { + let active_seg = tick % (SEG_LEN * 2); + #[allow(clippy::indexing_slicing, reason = "wraps")] + (0..SEG_LEN) + .map(|j| { + let seg_ch = CH_SEGMENT_ANIMATION[(tick + j) % (CH_SEGMENT_ANIMATION.len())]; + paint_fg( + Color::Blue, + (active_seg > j) || (active_seg + 10 < j + 10 - 2), + seg_ch, + ) + }) + .collect() + } + SegmentState::Pending => paint_fg( + Color::White, + true, + CH_SEGMENT_NOT_CONNECTED.to_string().repeat(SEG_LEN), + ), + } +} + +/// Render the full connection chain graph for a given animation tick +#[must_use] +pub fn render_connection_chain(chain: &VisualConnectionChainState, tick: usize) -> String { + let mut out = String::new(); + + for (seg_index, host) in chain.items.iter().enumerate() { + let state = if seg_index < chain.connected_hops + 1 { + SegmentState::Connected + } else if seg_index == chain.connected_hops + 1 { + SegmentState::Connecting + } else { + SegmentState::Pending + }; + + if seg_index > 0 { + out.push(' '); + out.push_str(&render_segment_line(state, tick)); + out.push(' '); + } + + out.push_str(&paint_fg( + match state { + SegmentState::Connected => Color::Green, + SegmentState::Connecting => Color::Blue, + SegmentState::Pending => Color::White, + }, + state == SegmentState::Pending, + match state { + SegmentState::Connected => CH_TARGET_CONNECTED, + _ => CH_TARGET_NOT_CONNECTED, + }, + )); + out.push(' '); + out.push_str(&paint_fg( + match state { + SegmentState::Connected => Color::White, + SegmentState::Connecting => Color::Blue, + SegmentState::Pending => Color::White, + }, + state == SegmentState::Pending, + host.ansi(), + )); + } + + out.push_str("\r\n"); + + out +} + +fn erase_for_width(w: usize) -> String { + format!("{CURSOR_UP}\r{}\r", " ".repeat(w)) +} + #[derive(Clone)] pub struct ServiceOutput { progress_visible: Arc, + last_progress_width: Arc, + chain: Arc>>, abort_tx: mpsc::Sender<()>, output_tx: broadcast::Sender, } @@ -27,27 +156,30 @@ pub struct ServiceOutput { impl ServiceOutput { pub fn new() -> Self { let progress_visible = Arc::new(AtomicBool::new(false)); + let last_progress_width = Arc::new(AtomicUsize::new(0)); + let chain: Arc>> = Arc::new(Mutex::new(None)); let (abort_tx, mut abort_rx) = mpsc::channel(1); let output_tx = broadcast::channel(32).0; tokio::spawn({ let output_tx = output_tx.clone(); + let last_progress_width = last_progress_width.clone(); let progress_visible = progress_visible.clone(); - let ticks = "⠁⠁⠉⠙⠚⠒⠂⠂⠒⠲⠴⠤⠄⠄⠤⠠⠠⠤⠦⠖⠒⠐⠐⠒⠓⠋⠉⠈⠈".chars().collect::>(); - let mut tick_index = 0; + let chain = chain.clone(); + let mut tick = 0usize; async move { loop { tokio::select! { - _ = abort_rx.recv() => { - return; - } - () = tokio::time::sleep(std::time::Duration::from_millis(100)) => { - if progress_visible.load(std::sync::atomic::Ordering::Relaxed) { - tick_index = (tick_index + 1) % ticks.len(); - #[allow(clippy::indexing_slicing)] - let tick = ticks[tick_index]; - let badge = ansi_paint(Color::Black, Color::Blue, &format!(" {tick} Warpgate connecting ")); - let _ = output_tx.send(Bytes::from([ERASE_PROGRESS_SPINNER_BUF, badge.as_bytes()].concat())); + _ = abort_rx.recv() => return, + () = tokio::time::sleep(ANIM_FRAME_DURATION) => { + if progress_visible.load(Ordering::Relaxed) { + tick += 1; + let guard = chain.lock().await; + if let Some(c) = &*guard { + let frame = format!("{CURSOR_UP}\r{}", render_connection_chain(c, tick)); + last_progress_width.store(frame.len(), Ordering::Relaxed); + let _ = output_tx.send(Bytes::from(frame.into_bytes())); + } } } } @@ -57,19 +189,63 @@ impl ServiceOutput { Self { progress_visible, + last_progress_width, + chain, abort_tx, output_tx, } } + pub async fn start_progress(&self, hosts: Vec) { + *self.chain.lock().await = Some(VisualConnectionChainState { + items: hosts, + connected_hops: 1, + }); + self.progress_visible.store(true, Ordering::Relaxed); + } + + pub async fn notify_hop_connected(&self) { + let mut guard = self.chain.lock().await; + if let Some(c) = &mut *guard { + c.connected_hops += 1; + } + } + + /// Re-enable the animation (e.g. after pausing for a host-key prompt). pub fn show_progress(&self) { - self.progress_visible - .store(true, std::sync::atomic::Ordering::Relaxed); + self.progress_visible.store(true, Ordering::Relaxed); } pub fn stop_progress(&self) { - self.progress_visible - .store(false, std::sync::atomic::Ordering::Relaxed); + self.progress_visible.store(false, Ordering::Relaxed); + } + + pub fn progress_visible(&self) -> bool { + self.progress_visible.load(Ordering::Relaxed) + } + + #[must_use] + pub async fn render_final_success_static_frame(&self) -> String { + self.progress_visible.store(false, Ordering::Relaxed); + let chain = self.chain.lock().await; + let graph = if let Some(c) = &*chain { + let n = c.items.len(); + let all_green = VisualConnectionChainState { + items: c.items.clone(), + connected_hops: n, + }; + render_connection_chain(&all_green, 0) + } else { + "".into() + }; + drop(chain); + format!("{}{}\r\n", self.erase_display(), graph) + } + + /// String that erases the last line + #[must_use] + pub fn erase_display(&self) -> String { + erase_for_width(self.last_progress_width.load(Ordering::Relaxed)) } pub fn subscribe(&self) -> broadcast::Receiver { diff --git a/warpgate-protocol-ssh/src/server/session.rs b/warpgate-protocol-ssh/src/server/session.rs index b35a7d855..1307aaa25 100644 --- a/warpgate-protocol-ssh/src/server/session.rs +++ b/warpgate-protocol-ssh/src/server/session.rs @@ -23,8 +23,7 @@ use warpgate_common::auth::{ }; use warpgate_common::eventhub::{EventHub, EventSender, EventSubscription}; use warpgate_common::{ - Secret, SessionId, SshHostKeyVerificationMode, Target, TargetOptions, TargetSSHOptions, - WarpgateError, + Secret, SessionId, SshHostKeyVerificationMode, Target, TargetOptions, WarpgateError, }; use warpgate_common_http::ext::construct_external_url; use warpgate_core::auth::validate_and_add_credential; @@ -39,16 +38,16 @@ use warpgate_db_entities::Parameters; use super::channel_writer::ChannelWriter; use super::russh_handler::ServerHandlerEvent; -use super::service_output::{ServiceOutput, ansi_paint}; +use super::service_output::ServiceOutput; use super::session_handle::SessionHandleCommand; use crate::compat::ContextExt; use crate::server::get_allowed_auth_methods; -use crate::server::service_output::ERASE_PROGRESS_SPINNER; +use crate::server::service_output::{VisualConnectionChainItem, paint_fg}; use crate::server::target_menu::{MenuEvent, spawn_target_menu_loop}; use crate::{ ChannelOperation, ConnectionError, DirectTCPIPParams, PtyRequest, RCCommand, RCCommandReply, - RCEvent, RCState, RemoteClient, ServerChannelId, SshClientError, SshRecordingMetadata, - X11Request, + RCEvent, RCState, RemoteClient, ResolvedSshChainHost, ServerChannelId, SshClientError, + SshRecordingMetadata, X11Request, resolve_ssh_chain, }; #[derive(Clone)] @@ -57,7 +56,7 @@ enum TargetSelection { None, Menu, NotFound(String), - Found(Target, TargetSSHOptions), + Found(Target), } #[derive(Debug)] @@ -323,20 +322,6 @@ impl ServerSession { .ok_or_else(|| anyhow::anyhow!("Channel not known")) } - pub fn emit_service_message(&self, msg: &str) -> Result<()> { - debug!("Service message: {}", msg); - - let output = format!( - "{}{} {}\r\n", - ERASE_PROGRESS_SPINNER, - ansi_paint(Color::Black, Color::White, " Warpgate "), - msg.replace('\n', "\r\n"), - ); - self.emit_pty_output(output.as_bytes())?; - - Ok(()) - } - pub fn emit_pty_output(&self, data: &[u8]) -> Result<()> { let channels = self.pty_channels.clone(); for channel in channels { @@ -348,6 +333,30 @@ impl ServerSession { Ok(()) } + pub fn emit_service_message(&self, msg: &str) -> Result<()> { + debug!("Service message: {}", msg); + + let _ = self.emit_pty_output(self.service_output.erase_display().as_bytes()); + self.emit_pty_output( + format!( + "{} {}\r\n", + paint_fg(Color::Blue, false, "● Warpgate:"), + msg.replace('\n', "\r\n") + ) + .as_bytes(), + ) + } + + pub fn emit_pty_error(&self, msg: &str) -> Result<()> { + if self.service_output.progress_visible() { + self.service_output.stop_progress(); + let _ = self.emit_pty_output(self.service_output.erase_display().as_bytes()); + } + self.emit_pty_output( + format!("{} {msg}\r\n", paint_fg(Color::Red, false, "● Warpgate:")).as_bytes(), + ) + } + /// Start connecting to the target if we aren't already. /// /// Timing of this call is important because if the client connection is @@ -359,7 +368,7 @@ impl ServerSession { /// where a PTY channel is required for the host key prompt, but we've connected /// faster than the client could open one. pub async fn maybe_connect_remote(&mut self) -> Result<()> { - let connect_params = match &self.target { + let target = match &self.target { TargetSelection::None => { anyhow::bail!("Invalid session state (target not set)") } @@ -370,30 +379,55 @@ impl ServerSession { self.disconnect_server().await; anyhow::bail!("Target not found: {name}"); } - TargetSelection::Found(target, ssh_options) => { - Some((target.clone(), ssh_options.clone())) - } + TargetSelection::Found(target) => Some(target.clone()), }; - if let Some((target, ssh_options)) = connect_params + if let Some(target) = target && self.rc_state == RCState::NotInitialized { - self.connect_remote(&target, ssh_options)?; + self.connect_remote(&target).await?; } Ok(()) } - fn connect_remote(&mut self, target: &Target, ssh_options: TargetSSHOptions) -> Result<()> { - self.rc_state = RCState::Connecting; - self.send_command(RCCommand::Connect(ssh_options)) - .map_err(|_| anyhow::anyhow!("cannot send command"))?; - self.service_output.show_progress(); - self.emit_service_message(&format!("Selected target: {}", target.name))?; + async fn connect_remote(&mut self, target: &Target) -> Result<()> { + let ssh_chain = + resolve_ssh_chain(&self.services, target.id, self.username.as_ref()).await?; + let visual_chain = self.make_visual_connection_chain(&ssh_chain[..]).await?; + self.rc_state = RCState::Connecting; + self.send_command(RCCommand::Connect( + ssh_chain.into_iter().map(|x| x.ssh_options).collect(), + )) + .map_err(|_| anyhow::anyhow!("cannot send command"))?; + self.emit_pty_output(b"\r\n")?; + self.service_output.start_progress(visual_chain).await; Ok(()) } + async fn make_visual_connection_chain( + &self, + ssh_chain: &[ResolvedSshChainHost], + ) -> Result, WarpgateError> { + let mut display = vec![ + VisualConnectionChainItem::Text("You".into()), + VisualConnectionChainItem::Link { + text: "Warpgate".into(), + url: construct_external_url(None, &*self.services.config.lock().await, None) + .await? + .to_string(), + }, + ]; + display.extend( + ssh_chain + .iter() + .map(|host| VisualConnectionChainItem::Text(host.name.clone())), + ); + + Ok(display) + } + async fn handle_menu_event(&mut self, action: MenuEvent) -> Result<()> { match action { MenuEvent::Render(data) => { @@ -404,8 +438,8 @@ impl ServerSession { self.request_disconnect(); self.disconnect_server().await; } - MenuEvent::Selected(target, ssh_options) => { - self.target = TargetSelection::Found(target.clone(), ssh_options.clone()); + MenuEvent::Selected(target) => { + self.target = TargetSelection::Found(target.clone()); let _ = self.server_handle.lock().await.set_target(&target).await; // clear screen ; cursor to 1;1 self.emit_pty_output(b"\x1b[2J\x1b[H")?; @@ -750,16 +784,17 @@ impl ServerSession { pub async fn handle_remote_event(&mut self, event: RCEvent) -> Result<()> { match event { + RCEvent::HopConnected => { + self.service_output.notify_hop_connected().await; + } RCEvent::State(state) => { self.rc_state = state; match &self.rc_state { RCState::Connected => { - self.service_output.stop_progress(); - let msg = format!( - "{}{}\r\n", - ERASE_PROGRESS_SPINNER, - ansi_paint(Color::Black, Color::Green, " ✓ Warpgate connected ") - ); + let msg = self + .service_output + .render_final_success_static_frame() + .await; let _ = self.emit_pty_output(msg.as_bytes()); } RCState::Disconnected => { @@ -779,12 +814,9 @@ impl ServerSession { known_key_type, known_key_base64, } => { + let _ = self.emit_pty_error("Host key doesn't match the stored one."); let msg = format!( - concat!( - "Host key doesn't match the stored one.\n", - "Stored key ({}): {}\n", - "Received key ({}): {}", - ), + concat!("Stored key ({}): {}\n", "Received key ({}): {}",), known_key_type, known_key_base64, received_key_type, @@ -800,31 +832,18 @@ impl ServerSession { ?; } ConnectionError::Authentication => { - let msg = format!( - "{}{}\r\n", - ERASE_PROGRESS_SPINNER, - ansi_paint( - Color::Black, - Color::Red, - " ✗ SSH target rejected Warpgate authentication request " - ) + let _ = self.emit_pty_error( + "SSH target rejected Warpgate's authentication request", ); - let _ = self.emit_pty_output(msg.as_bytes()); } error => { - let msg = format!( - "{}{} {}\r\n", - ERASE_PROGRESS_SPINNER, - ansi_paint(Color::Black, Color::Red, " ✗ Connection failed "), - error - ); - let _ = self.emit_pty_output(msg.as_bytes()); + let _ = self.emit_pty_error(&format!("Target connection failed: {error}")); } } } RCEvent::Error(e) => { self.service_output.stop_progress(); - let _ = self.emit_service_message(&format!("Error: {e}")); + let _ = self.emit_pty_error(&format!("Error: {e}")); self.disconnect_server().await; } RCEvent::Output(channel, data) => { @@ -949,13 +968,7 @@ impl ServerSession { .write_extended(session, server_channel_id.0, ext, data); } } - RCEvent::HostKeyReceived(key) => { - self.emit_service_message(&format!( - "Host key ({}): {}", - key.algorithm(), - key.public_key_base64() - ))?; - } + RCEvent::HostKeyReceived(_) => {} RCEvent::HostKeyUnknown(key, reply) => { self.handle_unknown_host_key(key, reply).await?; } @@ -1094,6 +1107,11 @@ impl ServerSession { anyhow::bail!("No PTY channel to show an interactive prompt on") } + self.emit_service_message(&format!( + "Host key ({}): {}", + key.algorithm(), + key.public_key_base64() + ))?; self.emit_service_message(&format!( "There is no trusted {} key for this host.", key.algorithm() @@ -1956,19 +1974,14 @@ impl ServerSession { .map(|(t, opt)| (t.clone(), opt.clone())) }; - let Some((target, mut ssh_options)) = target else { + let Some((target, _)) = target else { self.target = TargetSelection::NotFound(target_name.to_string()); warn!("Selected target not found"); return Ok(()); }; - // Forward username from the authenticated user to the target, if target has no username - if ssh_options.username.is_empty() { - ssh_options.username = user_info.username.clone(); - } - let _ = self.server_handle.lock().await.set_target(&target).await; - self.target = TargetSelection::Found(target, ssh_options); + self.target = TargetSelection::Found(target); Ok(()) } diff --git a/warpgate-protocol-ssh/src/server/target_menu.rs b/warpgate-protocol-ssh/src/server/target_menu.rs index d2f12d745..294322390 100644 --- a/warpgate-protocol-ssh/src/server/target_menu.rs +++ b/warpgate-protocol-ssh/src/server/target_menu.rs @@ -33,7 +33,7 @@ struct DrawState { #[allow(clippy::large_enum_variant)] pub enum MenuEvent { Render(Bytes), - Selected(Target, TargetSSHOptions), + Selected(Target), Abort, } @@ -413,8 +413,8 @@ pub fn spawn_target_menu_loop( Some(MenuEvent::Render(Bytes::from(menu.render()?))) } Some(MenuInputResult::Abort) => Some(MenuEvent::Abort), - Some(MenuInputResult::Selected((target, options))) => { - Some(MenuEvent::Selected(target, options)) + Some(MenuInputResult::Selected((target, _options))) => { + Some(MenuEvent::Selected(target)) } }; diff --git a/warpgate-web-ssh/src/manager.rs b/warpgate-web-ssh/src/manager.rs index 43be0df56..e16d14d79 100644 --- a/warpgate-web-ssh/src/manager.rs +++ b/warpgate-web-ssh/src/manager.rs @@ -16,7 +16,7 @@ use warpgate_core::recordings::TerminalRecordingStreamId; use warpgate_core::{ConfigProvider, Services, SessionStateInit, State}; use warpgate_db_entities::Target::TargetKind; use warpgate_protocol_ssh::known_hosts::KnownHosts; -use warpgate_protocol_ssh::{RCCommand, RCEvent, RCState, RemoteClient}; +use warpgate_protocol_ssh::{RCCommand, RCEvent, RCState, RemoteClient, resolve_ssh_chain}; use crate::protocol::ServerMessage; use crate::session::{WebSshSession, WebSshSessionHandle}; @@ -126,9 +126,14 @@ impl WebSshClientManager { .await .insert(session_id, session.clone()); + let ssh_chain = resolve_ssh_chain(services, target.id, Some(&username.to_string())) + .await? + .into_iter() + .map(|x| x.ssh_options) + .collect::>(); rc_handles .command_tx - .send((RCCommand::Connect(ssh_options.clone()), None)) + .send((RCCommand::Connect(ssh_chain), None)) .ok(); spawn_event_loop( diff --git a/warpgate-web/src/admin/config/targets/ssh/KeyChecker.svelte b/warpgate-web/src/admin/config/targets/ssh/KeyChecker.svelte index bee788468..8e372c3ce 100644 --- a/warpgate-web/src/admin/config/targets/ssh/KeyChecker.svelte +++ b/warpgate-web/src/admin/config/targets/ssh/KeyChecker.svelte @@ -1,5 +1,5 @@
-

getting started

+
+

getting started

+ +
@@ -50,7 +59,6 @@ h2 { font-family: 'Poppins'; font-weight: 700; - margin-bottom: 1rem; } .item-text { diff --git a/warpgate-web/src/gateway/lib/openapi-schema.json b/warpgate-web/src/gateway/lib/openapi-schema.json index c8ffc32ce..0e2be56a3 100644 --- a/warpgate-web/src/gateway/lib/openapi-schema.json +++ b/warpgate-web/src/gateway/lib/openapi-schema.json @@ -2,7 +2,7 @@ "openapi": "3.0.0", "info": { "title": "Warpgate HTTP proxy", - "version": "v0.24.0-23-g5c4ac819-modified" + "version": "v0.24.0-30-gcc876528-modified" }, "servers": [ { @@ -180,7 +180,7 @@ "description": "" } }, - "operationId": "get_foreign_auth_state" + "operationId": "get_auth_state" } }, "/auth/state/{id}/approve": { @@ -282,6 +282,19 @@ "operationId": "get_info" } }, + "/dismiss-tutorial": { + "post": { + "responses": { + "201": { + "description": "" + }, + "403": { + "description": "" + } + }, + "operationId": "dismiss_tutorial" + } + }, "/targets": { "get": { "parameters": [ @@ -2029,7 +2042,8 @@ "title": "SetupState", "required": [ "has_targets", - "has_users" + "has_users", + "tutorial_dismissed" ], "properties": { "has_targets": { @@ -2037,6 +2051,9 @@ }, "has_users": { "type": "boolean" + }, + "tutorial_dismissed": { + "type": "boolean" } } }, From f95795ca4b8d1e75d11268039f261ed223ddb32a Mon Sep 17 00:00:00 2001 From: Eugene Date: Sun, 7 Jun 2026 19:32:02 +0200 Subject: [PATCH 155/556] lint --- warpgate-admin/src/api/parameters.rs | 1 + .../src/api/password_credentials.rs | 2 +- warpgate-admin/src/api/users.rs | 4 + warpgate-common-http/src/request.rs | 8 +- warpgate-common/src/auth/state.rs | 10 +- warpgate-common/src/helpers/hash.rs | 1 + .../src/helpers/password_policy.rs | 2 +- warpgate-core/src/config_providers/db.rs | 6 +- warpgate-db-migrations/src/helpers.rs | 2 +- warpgate-protocol-http/src/api/common.rs | 6 +- warpgate-protocol-http/src/api/credentials.rs | 4 +- .../src/api/sso_provider_detail.rs | 10 +- warpgate-protocol-http/src/api/web_ssh.rs | 2 +- warpgate-protocol-postgres/src/client.rs | 2 +- warpgate-protocol-postgres/src/session.rs | 4 +- warpgate-protocol-ssh/src/client/mod.rs | 8 +- .../src/server/service_output.rs | 10 +- warpgate-protocol-ssh/src/server/session.rs | 26 +++-- .../src/server/target_menu.rs | 96 +++++++++++-------- warpgate-web-ssh/src/api.rs | 8 +- warpgate-web-ssh/src/manager.rs | 8 +- warpgate-web-ssh/src/session.rs | 35 +++---- 22 files changed, 130 insertions(+), 125 deletions(-) diff --git a/warpgate-admin/src/api/parameters.rs b/warpgate-admin/src/api/parameters.rs index 279d594b6..b0db0c1e8 100644 --- a/warpgate-admin/src/api/parameters.rs +++ b/warpgate-admin/src/api/parameters.rs @@ -143,6 +143,7 @@ impl Api { body.max_api_token_duration_seconds.map_or(NotSet, Set); parameters.record_scp = body.record_scp.map_or(NotSet, Set); + #[allow(clippy::cast_possible_wrap)] if let Some(ref policy) = body.password_policy { parameters.password_policy_min_length = Set(policy.min_length as i32); parameters.password_policy_require_uppercase = Set(policy.require_uppercase); diff --git a/warpgate-admin/src/api/password_credentials.rs b/warpgate-admin/src/api/password_credentials.rs index 37a50f92d..20e0f44b3 100644 --- a/warpgate-admin/src/api/password_credentials.rs +++ b/warpgate-admin/src/api/password_credentials.rs @@ -92,7 +92,7 @@ impl ListApi { let db = ctx.services().db.lock().await; - let parameters = Parameters::Entity::get(&*db).await?; + let parameters = Parameters::Entity::get(&db).await?; let policy = parameters.password_policy(); let violations = validate_password(body.password.expose_secret(), &policy); if !violations.is_empty() { diff --git a/warpgate-admin/src/api/users.rs b/warpgate-admin/src/api/users.rs index e38862f00..8bb15f6d4 100644 --- a/warpgate-admin/src/api/users.rs +++ b/warpgate-admin/src/api/users.rs @@ -39,7 +39,9 @@ enum GetUsersResponse { #[oai(status = 200)] Ok(Json>), } + #[derive(ApiResponse)] +#[allow(clippy::large_enum_variant)] enum CreateUserResponse { #[oai(status = 201)] Created(Json), @@ -162,6 +164,7 @@ enum DeleteUserResponse { } #[derive(ApiResponse)] +#[allow(clippy::large_enum_variant)] enum UnlinkUserFromLdapResponse { #[oai(status = 200)] Ok(Json), @@ -174,6 +177,7 @@ enum UnlinkUserFromLdapResponse { } #[derive(ApiResponse)] +#[allow(clippy::large_enum_variant)] enum AutoLinkUserToLdapResponse { #[oai(status = 200)] Ok(Json), diff --git a/warpgate-common-http/src/request.rs b/warpgate-common-http/src/request.rs index e0f3c5fde..9167669a1 100644 --- a/warpgate-common-http/src/request.rs +++ b/warpgate-common-http/src/request.rs @@ -3,14 +3,14 @@ use poem::http::header::HOST; use poem::http::uri::Scheme; use warpgate_common::http_headers::{X_FORWARDED_FOR, X_FORWARDED_HOST, X_FORWARDED_PROTO}; -pub(crate) fn first_forwarded_header_value(value: &str) -> Option<&str> { +pub fn first_forwarded_header_value(value: &str) -> Option<&str> { value .split(',') .map(str::trim) .find(|value| !value.is_empty()) } -pub(crate) fn trusted_host_header(should_trust_x_forwarded: bool, req: &Request) -> Option { +pub fn trusted_host_header(should_trust_x_forwarded: bool, req: &Request) -> Option { if should_trust_x_forwarded && let Some(host) = req .header(&X_FORWARDED_HOST) @@ -25,7 +25,7 @@ pub(crate) fn trusted_host_header(should_trust_x_forwarded: bool, req: &Request) }) } -pub(crate) fn trusted_proto(should_trust_x_forwarded: bool, req: &Request) -> Scheme { +pub fn trusted_proto(should_trust_x_forwarded: bool, req: &Request) -> Scheme { if should_trust_x_forwarded && let Some(proto) = req .header(&X_FORWARDED_PROTO) @@ -41,7 +41,7 @@ pub(crate) fn trusted_proto(should_trust_x_forwarded: bool, req: &Request) -> Sc } } -pub(crate) fn trusted_client_ip( +pub fn trusted_client_ip( req: &Request, remote_ip: Option, trust_x_forwarded: bool, diff --git a/warpgate-common/src/auth/state.rs b/warpgate-common/src/auth/state.rs index 201f2a03f..dc45a0088 100644 --- a/warpgate-common/src/auth/state.rs +++ b/warpgate-common/src/auth/state.rs @@ -97,7 +97,7 @@ impl AuthState { self.session_id.as_ref() } - pub fn set_session_id(&mut self, session_id: SessionId) { + pub const fn set_session_id(&mut self, session_id: SessionId) { self.session_id = Some(session_id); } @@ -140,8 +140,7 @@ impl AuthState { fn client_ip_for_logging(&self) -> String { self.remote_ip - .map(|x| x.to_string()) - .unwrap_or_else(|| "".to_string()) + .map_or_else(|| "".to_string(), |x| x.to_string()) } pub fn emit_authenticated_event_once(&mut self) { @@ -181,9 +180,8 @@ impl AuthState { return; }; - let credentials = credential - .map(AuthCredential::safe_description) - .unwrap_or_else(|| "".to_string()); + let credentials = + credential.map_or_else(|| "".to_string(), AuthCredential::safe_description); info!( target: "audit", diff --git a/warpgate-common/src/helpers/hash.rs b/warpgate-common/src/helpers/hash.rs index 0291eba43..68a714e53 100644 --- a/warpgate-common/src/helpers/hash.rs +++ b/warpgate-common/src/helpers/hash.rs @@ -15,6 +15,7 @@ pub fn hash_password(password: &str) -> String { { Argon2::default() } else { + #[allow(clippy::unwrap_used, reason = "tests")] Argon2::new( argon2::Algorithm::Argon2id, argon2::Version::V0x13, diff --git a/warpgate-common/src/helpers/password_policy.rs b/warpgate-common/src/helpers/password_policy.rs index 2e6378ed9..b3f87a2e2 100644 --- a/warpgate-common/src/helpers/password_policy.rs +++ b/warpgate-common/src/helpers/password_policy.rs @@ -13,7 +13,7 @@ pub struct PasswordPolicy { } impl PasswordPolicy { - pub fn is_empty(&self) -> bool { + pub const fn is_empty(&self) -> bool { self.min_length == 0 && !self.require_uppercase && !self.require_lowercase diff --git a/warpgate-core/src/config_providers/db.rs b/warpgate-core/src/config_providers/db.rs index 6b04ff375..5362426a1 100644 --- a/warpgate-core/src/config_providers/db.rs +++ b/warpgate-core/src/config_providers/db.rs @@ -280,10 +280,8 @@ impl ConfigProvider for DatabaseConfigProvider { let hostname_query = match db.get_database_backend() { DatabaseBackend::MySql => Expr::cust("options->>'$.http.external_host'"), - DatabaseBackend::Postgres => Expr::cust(r#"options->'http'->>'external_host'"#), - DatabaseBackend::Sqlite => { - Expr::cust(r#"json_extract(options, '$.http.external_host')"#) - } + DatabaseBackend::Postgres => Expr::cust(r"options->'http'->>'external_host'"), + DatabaseBackend::Sqlite => Expr::cust(r"json_extract(options, '$.http.external_host')"), }; let target = entities::Target::Entity::find() diff --git a/warpgate-db-migrations/src/helpers.rs b/warpgate-db-migrations/src/helpers.rs index 3abe3a9b1..4a8e5cf30 100644 --- a/warpgate-db-migrations/src/helpers.rs +++ b/warpgate-db-migrations/src/helpers.rs @@ -3,7 +3,7 @@ use sea_orm::prelude::Expr; use sea_orm::sea_query::SimpleExpr; /// MySQL 8.0.13+ requires expression defaults (parenthesised) for TEXT columns -pub(crate) fn string_default_value(backend: DbBackend, value: &str) -> SimpleExpr { +pub fn string_default_value(backend: DbBackend, value: &str) -> SimpleExpr { if backend == DbBackend::MySql { Expr::cust(format!("('{value}')")) } else { diff --git a/warpgate-protocol-http/src/api/common.rs b/warpgate-protocol-http/src/api/common.rs index f8e9d2908..119c144f5 100644 --- a/warpgate-protocol-http/src/api/common.rs +++ b/warpgate-protocol-http/src/api/common.rs @@ -7,16 +7,14 @@ use warpgate_db_entities as entities; use crate::session::SessionStore; -pub(crate) fn emit_unknown_authentication_failed_event( +pub fn emit_unknown_authentication_failed_event( session_id: SessionId, remote_ip: Option, username: &str, credentials: &str, reason: &str, ) { - let client_ip = remote_ip - .map(|x| x.to_string()) - .unwrap_or_else(|| "".to_string()); + let client_ip = remote_ip.map_or_else(|| "".to_string(), |x| x.to_string()); info!( target: "audit", diff --git a/warpgate-protocol-http/src/api/credentials.rs b/warpgate-protocol-http/src/api/credentials.rs index f7b8a264e..d0e14ca11 100644 --- a/warpgate-protocol-http/src/api/credentials.rs +++ b/warpgate-protocol-http/src/api/credentials.rs @@ -284,7 +284,7 @@ impl Api { .all(&*db) .await?; - let parameters = Parameters::Entity::get(&*db).await?; + let parameters = Parameters::Entity::get(&db).await?; Ok(CredentialsStateResponse::Ok(Json(CredentialsState { password: match password_creds.len() { @@ -321,7 +321,7 @@ impl Api { return Ok(ChangePasswordResponse::Unauthorized); }; - let parameters = Parameters::Entity::get(&*db).await?; + let parameters = Parameters::Entity::get(&db).await?; let policy = parameters.password_policy(); let violations = validate_password(&body.password, &policy); if !violations.is_empty() { diff --git a/warpgate-protocol-http/src/api/sso_provider_detail.rs b/warpgate-protocol-http/src/api/sso_provider_detail.rs index 429e1157d..3b2c9dd1a 100644 --- a/warpgate-protocol-http/src/api/sso_provider_detail.rs +++ b/warpgate-protocol-http/src/api/sso_provider_detail.rs @@ -74,12 +74,10 @@ impl Api { ) && let (Some(request_host), Some(external_host)) = ( ctx.trusted_hostname(req), config.store.external_host.as_deref(), - ) { - if !is_localhost_host(&request_host) - && !host_is_subdomain_of_or_equal(&request_host, external_host) - { - return Ok(StartSsoResponse::IncompatibleSsoDomain); - } + ) && !is_localhost_host(&request_host) + && !host_is_subdomain_of_or_equal(&request_host, external_host) + { + return Ok(StartSsoResponse::IncompatibleSsoDomain); } let mut return_url = construct_external_url( diff --git a/warpgate-protocol-http/src/api/web_ssh.rs b/warpgate-protocol-http/src/api/web_ssh.rs index c89832878..8bafd02d9 100644 --- a/warpgate-protocol-http/src/api/web_ssh.rs +++ b/warpgate-protocol-http/src/api/web_ssh.rs @@ -110,7 +110,7 @@ impl Api { user_id, username, &target.name, - remote_addr.0.as_socket_addr().cloned(), + remote_addr.0.as_socket_addr().copied(), ) .await; diff --git a/warpgate-protocol-postgres/src/client.rs b/warpgate-protocol-postgres/src/client.rs index a7252471d..7c7940144 100644 --- a/warpgate-protocol-postgres/src/client.rs +++ b/warpgate-protocol-postgres/src/client.rs @@ -52,7 +52,7 @@ impl Write for SaslBufferWriter<'_> { } impl PostgresClient { - pub fn protocol_version(&self) -> ProtocolVersion { + pub const fn protocol_version(&self) -> ProtocolVersion { self.decode_context.protocol_version } diff --git a/warpgate-protocol-postgres/src/session.rs b/warpgate-protocol-postgres/src/session.rs index f2c025f0a..a52fb05bf 100644 --- a/warpgate-protocol-postgres/src/session.rs +++ b/warpgate-protocol-postgres/src/session.rs @@ -576,7 +576,7 @@ impl PostgresSession { let downgraded_key = SecretKey::I32(rand::random::()); self.cancel_key_downgrade_map .insert(downgraded_key.clone(), SecretKey::Bytes(bytes.clone())); - key_data.secret_key = downgraded_key + key_data.secret_key = downgraded_key; } } if self.decode_context.protocol_version == ProtocolVersion::PROTOCOL3_2 { @@ -586,7 +586,7 @@ impl PostgresSession { let upgraded_key = SecretKey::Bytes(Bytes::from_owner(value)); self.cancel_key_upgrade_map .insert(upgraded_key.clone(), key_data.secret_key.clone()); - key_data.secret_key = upgraded_key + key_data.secret_key = upgraded_key; } } } diff --git a/warpgate-protocol-ssh/src/client/mod.rs b/warpgate-protocol-ssh/src/client/mod.rs index 27ffdde71..2ec04be2a 100644 --- a/warpgate-protocol-ssh/src/client/mod.rs +++ b/warpgate-protocol-ssh/src/client/mod.rs @@ -13,7 +13,7 @@ use bytes::Bytes; use channel_direct_tcpip::DirectTCPIPChannel; use channel_session::SessionChannel; pub use error::SshClientError; -use futures::pin_mut; +use futures::{FutureExt, pin_mut}; use handler::ClientHandler; use russh::client::{AuthResult, Handle, KeyboardInteractiveAuthResponse}; use russh::keys::{PrivateKeyWithHashAlg, PublicKey}; @@ -580,6 +580,7 @@ impl RemoteClient { let fut = russh::client::connect(config, address, handler); let (mut session, mut active_rx) = self .wait_for_connection(&first, fut, event_rx, false) + .boxed() .await?; for ssh_options in iter { @@ -592,7 +593,7 @@ impl RemoteClient { let channel = session .channel_open_direct_tcpip( ssh_options.host.clone(), - ssh_options.port as u32, + u32::from(ssh_options.port), "localhost".to_string(), 0, ) @@ -610,6 +611,7 @@ impl RemoteClient { let fut = russh::client::connect_stream(config, stream, handler); let (new_session, new_rx) = self .wait_for_connection(&ssh_options, fut, event_rx, false) + .boxed() .await?; session = new_session; active_rx = new_rx; @@ -619,7 +621,7 @@ impl RemoteClient { } async fn connect(&mut self, chain: Vec) -> Result<(), ConnectionError> { - let (session, mut event_rx) = self.connect_chain(chain).await?; + let (session, mut event_rx) = self.connect_chain(chain).boxed().await?; self.session = Some(Arc::new(Mutex::new(session))); diff --git a/warpgate-protocol-ssh/src/server/service_output.rs b/warpgate-protocol-ssh/src/server/service_output.rs index 0ce1c96c9..aaa035a92 100644 --- a/warpgate-protocol-ssh/src/server/service_output.rs +++ b/warpgate-protocol-ssh/src/server/service_output.rs @@ -35,10 +35,10 @@ pub enum VisualConnectionChainItem { } impl VisualConnectionChainItem { - pub fn ansi<'a>(&'a self) -> Cow<'a, str> { + pub fn ansi(&self) -> Cow<'_, str> { match self { - VisualConnectionChainItem::Text(s) => Cow::Borrowed(s), - VisualConnectionChainItem::Link { text, url } => { + Self::Text(s) => Cow::Borrowed(s), + Self::Link { text, url } => { Cow::Owned(format!("\x1b]8;;{url}\x1b\\{text}\x1b]8;;\x1b\\")) } } @@ -97,6 +97,7 @@ pub fn render_connection_chain(chain: &VisualConnectionChainState, tick: usize) let mut out = String::new(); for (seg_index, host) in chain.items.iter().enumerate() { + #[allow(clippy::comparison_chain)] let state = if seg_index < chain.connected_hops + 1 { SegmentState::Connected } else if seg_index == chain.connected_hops + 1 { @@ -126,9 +127,8 @@ pub fn render_connection_chain(chain: &VisualConnectionChainState, tick: usize) out.push(' '); out.push_str(&paint_fg( match state { - SegmentState::Connected => Color::White, SegmentState::Connecting => Color::Blue, - SegmentState::Pending => Color::White, + SegmentState::Connected | SegmentState::Pending => Color::White, }, state == SegmentState::Pending, host.ansi(), diff --git a/warpgate-protocol-ssh/src/server/session.rs b/warpgate-protocol-ssh/src/server/session.rs index 2b820ccce..078be962c 100644 --- a/warpgate-protocol-ssh/src/server/session.rs +++ b/warpgate-protocol-ssh/src/server/session.rs @@ -490,15 +490,14 @@ impl ServerSession { error!(?err, "Menu loop action handler error"); } } - Event::MenuRedraw(_, _) => (), - Event::ConsoleInput(_) => (), + Event::MenuRedraw(_, _) | Event::ConsoleInput(_) => (), } Ok(()) } .boxed() } - async fn start_target_selection_menu(&mut self, channel_id: Uuid) -> Result<()> { + async fn start_target_selection_menu(&self, channel_id: Uuid) -> Result<()> { let menu_event_subscription = self .hub .subscribe(|e| matches!(e, Event::MenuRedraw(_, _) | Event::ConsoleInput(_))) @@ -548,8 +547,7 @@ impl ServerSession { let (terminal_width, terminal_height) = self .channel_pty_size_map .get(&channel_id) - .map(|r| (r.col_width as u16, r.row_height as u16)) - .unwrap_or((220, 24)); + .map_or((220, 24), |r| (r.col_width as u16, r.row_height as u16)); spawn_target_menu_loop( self.id, @@ -952,7 +950,6 @@ impl ServerSession { }) .await?; } - RCEvent::Done => {} RCEvent::ExtendedData { channel, data, ext } => { if let Some(recorder) = self.channel_recorders.get_mut(&channel) && let Err(error) = recorder @@ -968,7 +965,7 @@ impl ServerSession { .write_extended(session, server_channel_id.0, ext, data); } } - RCEvent::HostKeyReceived(_) => {} + RCEvent::Done | RCEvent::HostKeyReceived(_) => {} RCEvent::HostKeyUnknown(key, reply) => { self.handle_unknown_host_key(key, reply).await?; } @@ -1746,6 +1743,7 @@ impl ServerSession { )); auth_prompts.push(("Press Enter when done: ".into(), true)); + #[allow(clippy::items_after_statements)] const MAX_RETRIES: u8 = 3; if let Some(retries) = pending_web_auth_retries { if retries >= MAX_RETRIES { @@ -1760,21 +1758,21 @@ impl ServerSession { next_pending.web_approval_retry_count = Some(retries + 1); } else { next_pending.web_approval_retry_count = Some(0); - }; + } } - if !auth_prompts.is_empty() { + if auth_prompts.is_empty() { + russh::server::Auth::Reject { + proceed_with_methods: None, + partial_success: false, + } + } else { self.keyboard_interactive_state = Some(next_pending); russh::server::Auth::Partial { name: auth_name.into(), instructions: auth_instructions.into(), prompts: auth_prompts.into(), } - } else { - russh::server::Auth::Reject { - proceed_with_methods: None, - partial_success: false, - } } } Err(error) => { diff --git a/warpgate-protocol-ssh/src/server/target_menu.rs b/warpgate-protocol-ssh/src/server/target_menu.rs index 294322390..ddb1d5107 100644 --- a/warpgate-protocol-ssh/src/server/target_menu.rs +++ b/warpgate-protocol-ssh/src/server/target_menu.rs @@ -1,4 +1,5 @@ use std::io::Cursor; +use std::ops::Deref; use bytes::Bytes; use ratatui::backend::CrosstermBackend; @@ -104,11 +105,13 @@ impl TargetMenu { continue; }; match (key_event.key, key_event.modifiers) { - (KeyCode::UpArrow | KeyCode::ApplicationUpArrow, _) => { + (KeyCode::Char('k' | 'K'), Modifiers::NONE) + | (KeyCode::UpArrow | KeyCode::ApplicationUpArrow, _) => { self.move_up(); redraw = true; } - (KeyCode::DownArrow | KeyCode::ApplicationDownArrow, _) => { + (KeyCode::Char('j' | 'J'), Modifiers::NONE) + | (KeyCode::DownArrow | KeyCode::ApplicationDownArrow, _) => { self.move_down(); redraw = true; } @@ -124,8 +127,8 @@ impl TargetMenu { let visible_indices = self.visible_indices(); let sel = self.list_state.selected().unwrap_or(0); if let Some(&entry_idx) = visible_indices.get(sel) { - let selected = self.entries[entry_idx].value.clone(); - return Some(MenuInputResult::Selected(selected)); + let selected = self.entries.get(entry_idx).map(|e| e.value.clone()); + return selected.map(MenuInputResult::Selected); } } (KeyCode::Backspace, _) => { @@ -145,14 +148,6 @@ impl TargetMenu { (KeyCode::Char('c'), modifiers) if modifiers.contains(Modifiers::CTRL) => { return Some(MenuInputResult::Abort); } - (KeyCode::Char('k' | 'K'), Modifiers::NONE) => { - self.move_up(); - redraw = true; - } - (KeyCode::Char('j' | 'J'), Modifiers::NONE) => { - self.move_down(); - redraw = true; - } (KeyCode::Char(ch), Modifiers::NONE) if ch.is_ascii_graphic() || ch == ' ' => { self.filter_input.handle(InputRequest::InsertChar(ch)); self.list_state.select(Some(0)); @@ -184,7 +179,8 @@ impl TargetMenu { Some( visible_indices .iter() - .map(|&i| ListItem::new(self.entries[i].label.clone())) + .filter_map(|&i| self.entries.get(i)) + .map(|e| ListItem::new(e.label.clone())) .collect(), ) }; @@ -216,49 +212,75 @@ impl TargetMenu { } self.terminal.draw(|frame| { - let areas = Layout::vertical([Constraint::Length(HEADER_HEIGHT), Constraint::Min(1)]) - .split(frame.area()); - let header_area = areas[0]; - let body_area = areas[1]; + let [header_area, body_area]: [Rect; 2] = { + let areas = + Layout::vertical([Constraint::Length(HEADER_HEIGHT), Constraint::Min(1)]) + .split(frame.area()); + #[allow(clippy::unwrap_used, reason = "hardcoded size")] + areas.deref().try_into().unwrap() + }; let header_block = Block::default() .border_style(Style::default().fg(Color::DarkGray)) .border_type(BorderType::Plain) .borders(Borders::BOTTOM); let header_block_area = header_block.inner(header_area); - let header_block_areas = - Layout::vertical([Constraint::Length(1); 5].as_slice()).split(header_block_area); + + let [ + header_block_area_subdiv_0, + _, + header_block_area_subdiv_2, + _, + header_block_area_subdiv_4, + ]: [Rect; 5] = { + let header_block_area_subdivs = + Layout::vertical([Constraint::Length(1); 5].as_slice()) + .split(header_block_area); + #[allow(clippy::unwrap_used, reason = "hardcoded size")] + header_block_area_subdivs.deref().try_into().unwrap() + }; frame.render_widget(header_block, header_area); frame.render_widget( Paragraph::new( Line::from("↑ / ↓ / Enter to connect. Type to filter. Ctrl-C to exit.").gray(), ), - header_block_areas[2], + header_block_area_subdiv_2, ); - // let title_row = Rect::new(header_area.x, header_area.y, header_area.width, 1); - let title_cols = Layout::horizontal([ - Constraint::Min(0), - Constraint::Length(draw_state.username_display.chars().count() as u16), - ]) - .split(header_block_areas[0]); - frame.render_widget(Paragraph::new("Welcome to Warpgate"), title_cols[0]); + let [title_col_0, title_col_1]: [Rect; 2] = { + let title_cols = Layout::horizontal([ + Constraint::Min(0), + Constraint::Length(draw_state.username_display.chars().count() as u16), + ]) + .split(header_block_area_subdiv_0); + + #[allow(clippy::unwrap_used, reason = "hardcoded size")] + title_cols.deref().try_into().unwrap() + }; + + frame.render_widget(Paragraph::new("Welcome to Warpgate"), title_col_0); frame.render_widget( Paragraph::new(Line::from(draw_state.username_display.clone().gray())), - title_cols[1], + title_col_1, ); - let filter_cols = Layout::horizontal([Constraint::Length(8), Constraint::Min(0)]) - .split(header_block_areas[4]); - frame.render_widget(Paragraph::new("Filter: "), filter_cols[0]); + let [filter_col_0, filter_col_1]: [Rect; 2] = { + let filter_cols = Layout::horizontal([Constraint::Length(8), Constraint::Min(0)]) + .split(header_block_area_subdiv_4); + + #[allow(clippy::unwrap_used, reason = "hardcoded size")] + filter_cols.deref().try_into().unwrap() + }; + + frame.render_widget(Paragraph::new("Filter: "), filter_col_0); frame.render_widget( Paragraph::new(draw_state.filter_value.as_str()), - filter_cols[1], + filter_col_1, ); frame.set_cursor_position(( - filter_cols[1].x + draw_state.filter_cursor as u16, - filter_cols[1].y, + filter_col_1.x + draw_state.filter_cursor as u16, + filter_col_1.y, )); if let Some(items) = draw_state.list_items.take() { @@ -418,10 +440,8 @@ pub fn spawn_target_menu_loop( } }; - let terminal = matches!( - action, - Some(MenuEvent::Selected(..)) | Some(MenuEvent::Abort) - ); + let terminal = + matches!(action, Some(MenuEvent::Selected(..) | MenuEvent::Abort)); if terminal { // restore terminal state diff --git a/warpgate-web-ssh/src/api.rs b/warpgate-web-ssh/src/api.rs index a80e11c5e..ef9432463 100644 --- a/warpgate-web-ssh/src/api.rs +++ b/warpgate-web-ssh/src/api.rs @@ -55,7 +55,7 @@ pub async fn ws_handler( loop { tokio::select! { - _ = session.wait_buffer() => { + () = session.wait_buffer() => { let msgs = session.drain_buffer().await; for msg in msgs { if let Ok(json) = serde_json::to_string(&msg) @@ -71,13 +71,13 @@ pub async fn ws_handler( maybe_msg = stream.next() => { match maybe_msg { Some(Ok(Message::Text(text))) => { + #[allow(clippy::collapsible_if)] if let Ok(client_msg) = serde_json::from_str::(&text) && let Some(reply) = handle_client_message(&session, &db, client_msg).await && let Ok(json) = serde_json::to_string(&reply) { if sink.send(Message::Text(json)).await.is_err() { break; } - } } Some(Ok(Message::Close(_))) | None => break, @@ -115,7 +115,7 @@ async fn handle_client_message( Some(ServerMessage::ChannelOpened { channel_id }) } ClientMessage::Input { channel_id, data } => { - session.send_input(channel_id, data.0).await; + session.send_input(channel_id, data.0); None } ClientMessage::Resize { @@ -127,7 +127,7 @@ async fn handle_client_message( None } ClientMessage::CloseChannel { channel_id } => { - session.close_channel(channel_id).await; + session.close_channel(channel_id); None } ClientMessage::AcceptHostKey => { diff --git a/warpgate-web-ssh/src/manager.rs b/warpgate-web-ssh/src/manager.rs index e7fe6b8a0..622b39822 100644 --- a/warpgate-web-ssh/src/manager.rs +++ b/warpgate-web-ssh/src/manager.rs @@ -198,17 +198,13 @@ fn spawn_event_loop( RCEvent::Eof(channel_id) => { session.push_event(ServerMessage::Eof { channel_id }).await; } - RCEvent::Close(channel_id) => { - session.stop_recording(channel_id).await; - session - .push_event(ServerMessage::ChannelClosed { channel_id }) - .await; - } + RCEvent::ExitStatus(channel_id, code) => { session .push_event(ServerMessage::ExitStatus { channel_id, code }) .await; } + RCEvent::Close(channel_id) | RCEvent::ChannelFailure(channel_id) => { session.stop_recording(channel_id).await; session diff --git a/warpgate-web-ssh/src/session.rs b/warpgate-web-ssh/src/session.rs index af3ae66ed..1251c0959 100644 --- a/warpgate-web-ssh/src/session.rs +++ b/warpgate-web-ssh/src/session.rs @@ -35,7 +35,7 @@ pub struct WebSshSessionHandle { } impl WebSshSessionHandle { - pub fn new(abort_tx: UnboundedSender<()>) -> Self { + pub const fn new(abort_tx: UnboundedSender<()>) -> Self { Self { abort_tx } } } @@ -123,11 +123,11 @@ impl WebSshSession { let _ = self.abort_tx.send(()); } - pub fn id(&self) -> Uuid { + pub const fn id(&self) -> Uuid { self.id } - pub fn user_id(&self) -> Uuid { + pub const fn user_id(&self) -> Uuid { self.user_id } @@ -135,7 +135,7 @@ impl WebSshSession { &self.target_name } - pub fn target_kind(&self) -> &TargetKind { + pub const fn target_kind(&self) -> &TargetKind { &self.target_kind } @@ -213,10 +213,7 @@ impl WebSshSession { self.output_notify.notify_waiters(); } - async fn command( - &self, - cmd: RCCommand, - ) -> Option>> { + fn command(&self, cmd: RCCommand) -> Option>> { let (tx, rx) = oneshot::channel(); if self.command_tx.send((cmd, Some(tx))).is_err() { @@ -239,32 +236,27 @@ impl WebSshSession { }) .await; - self.command(RCCommand::Channel(channel_id, ChannelOperation::OpenShell)) - .await; + self.command(RCCommand::Channel(channel_id, ChannelOperation::OpenShell)); self.command(RCCommand::Channel( channel_id, ChannelOperation::RequestPty(make_pty_request(cols, rows)), - )) - .await; + )); self.command(RCCommand::Channel( channel_id, ChannelOperation::RequestShell, - )) - .await; + )); channel_id } - pub async fn send_input(&self, channel_id: Uuid, data: Bytes) { - self.command(RCCommand::Channel(channel_id, ChannelOperation::Data(data))) - .await; + pub fn send_input(&self, channel_id: Uuid, data: Bytes) { + self.command(RCCommand::Channel(channel_id, ChannelOperation::Data(data))); } pub async fn resize_channel(&self, channel_id: Uuid, cols: u32, rows: u32) { self.command(RCCommand::Channel( channel_id, ChannelOperation::ResizePty(make_pty_request(cols, rows)), - )) - .await; + )); self.with_recorder(channel_id, async move |r| { if let Err(e) = r.write_pty_resize(cols, rows).await { error!(%channel_id, ?e, "Failed to record PTY resize"); @@ -273,9 +265,8 @@ impl WebSshSession { .await; } - pub async fn close_channel(&self, channel_id: Uuid) { - self.command(RCCommand::Channel(channel_id, ChannelOperation::Close)) - .await; + pub fn close_channel(&self, channel_id: Uuid) { + self.command(RCCommand::Channel(channel_id, ChannelOperation::Close)); } } From cc257eaeefacba8a7857d3423548f6c00a1fc457 Mon Sep 17 00:00:00 2001 From: Eugene Date: Sun, 7 Jun 2026 20:56:11 +0200 Subject: [PATCH 156/556] fixed SSO username misalignment --- warpgate-web/src/common/AuthBar.svelte | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/warpgate-web/src/common/AuthBar.svelte b/warpgate-web/src/common/AuthBar.svelte index 36df1fb29..cd79433bc 100644 --- a/warpgate-web/src/common/AuthBar.svelte +++ b/warpgate-web/src/common/AuthBar.svelte @@ -19,6 +19,7 @@ async function singleLogout () { {#if $serverInfo?.username} +
{$serverInfo.username} @@ -28,7 +29,7 @@ async function singleLogout () { {#if $serverInfo?.authorizedViaSsoWithSingleLogout} - + @@ -43,8 +44,9 @@ async function singleLogout () { {:else} - {/if} +
{/if} From 02044a7da27531918d278f4b5fc82cd340902763 Mon Sep 17 00:00:00 2001 From: Eugene Date: Sun, 7 Jun 2026 22:07:09 +0200 Subject: [PATCH 157/556] better error message for unverified SSO emails --- warpgate-protocol-http/src/api/sso_provider_list.rs | 7 +++++++ 1 file changed, 7 insertions(+) diff --git a/warpgate-protocol-http/src/api/sso_provider_list.rs b/warpgate-protocol-http/src/api/sso_provider_list.rs index 6b4498774..df88f1818 100644 --- a/warpgate-protocol-http/src/api/sso_provider_list.rs +++ b/warpgate-protocol-http/src/api/sso_provider_list.rs @@ -222,6 +222,13 @@ impl Api { })?; if !response.email_verified.unwrap_or(true) { + error!( + "SSO login attempt with an unverified email: {:?}", + response.email + ); + error!( + "The SSO provider did provide an email_verified claim, and it is false. Since the provider provides this claim, Warpgate requires the email to be verified." + ); return Ok(Err("The SSO account's e-mail is not verified".to_string())); } From b327406d537a33c5824e72957d3ec716d4bb4120 Mon Sep 17 00:00:00 2001 From: Eugene Date: Sun, 7 Jun 2026 22:11:11 +0200 Subject: [PATCH 158/556] lint --- warpgate-web/src/common/duration.ts | 4 ++-- warpgate-web/src/gateway/CreateApiTokenModal.svelte | 2 +- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/warpgate-web/src/common/duration.ts b/warpgate-web/src/common/duration.ts index c114e149b..0e8787f12 100644 --- a/warpgate-web/src/common/duration.ts +++ b/warpgate-web/src/common/duration.ts @@ -1,4 +1,4 @@ -import { SvelteActionReturnType } from 'svelte' +import type { ActionReturn } from 'svelte/action' /** * Format seconds into a string compatible with the humantime crate @@ -64,7 +64,7 @@ export function parseHumantimeDuration (str: string): number | undefined { export function humantimeDuration ( node: HTMLInputElement, params: { seconds: number | undefined; onChange: (seconds: number | undefined) => void }, -): SvelteActionReturnType { +): ActionReturn { node.value = params.seconds != null ? formatDurationAsHumantime(params.seconds) : '' function handleChange () { diff --git a/warpgate-web/src/gateway/CreateApiTokenModal.svelte b/warpgate-web/src/gateway/CreateApiTokenModal.svelte index 73bc461b6..634e0a4f2 100644 --- a/warpgate-web/src/gateway/CreateApiTokenModal.svelte +++ b/warpgate-web/src/gateway/CreateApiTokenModal.svelte @@ -73,7 +73,7 @@ type="datetime-local" max={maxExpiry} bind:value={expiry} /> - {#if maxDurationMs !== Number.POSITIVE_INFINITY} + {#if maxDurationMs !== null} Maximum: {Math.floor(maxDurationMs / 86400 / 1000)} days From 59c7093af80b9dc561c936a9ead20879ea9a409a Mon Sep 17 00:00:00 2001 From: Eugene Date: Sun, 7 Jun 2026 22:11:18 +0200 Subject: [PATCH 159/556] bump version to 0.25.0 --- bumpver.toml | 2 +- helm/warpgate/Chart.yaml | 2 +- helm/warpgate/values.yaml | 2 +- warpgate-admin/Cargo.toml | 2 +- warpgate-ca/Cargo.toml | 2 +- warpgate-common-http/Cargo.toml | 2 +- warpgate-common/Cargo.toml | 2 +- warpgate-core/Cargo.toml | 2 +- warpgate-database-protocols/Cargo.toml | 2 +- warpgate-db-entities/Cargo.toml | 2 +- warpgate-db-migrations/Cargo.toml | 2 +- warpgate-ldap/Cargo.toml | 2 +- warpgate-protocol-http/Cargo.toml | 2 +- warpgate-protocol-kubernetes/Cargo.toml | 2 +- warpgate-protocol-mysql/Cargo.toml | 2 +- warpgate-protocol-postgres/Cargo.toml | 2 +- warpgate-protocol-ssh/Cargo.toml | 2 +- warpgate-sso/Cargo.toml | 2 +- warpgate-tls/Cargo.toml | 2 +- warpgate-web/Cargo.toml | 2 +- warpgate/Cargo.toml | 2 +- 21 files changed, 21 insertions(+), 21 deletions(-) diff --git a/bumpver.toml b/bumpver.toml index e9a9d48b1..f4fe74d93 100644 --- a/bumpver.toml +++ b/bumpver.toml @@ -1,5 +1,5 @@ [bumpver] -current_version = "0.24.0" +current_version = "0.25.0" version_pattern = "MAJOR.MINOR.PATCH[-TAG[.INC0]]" commit = true tag = false diff --git a/helm/warpgate/Chart.yaml b/helm/warpgate/Chart.yaml index 6d78b6676..c691eb829 100644 --- a/helm/warpgate/Chart.yaml +++ b/helm/warpgate/Chart.yaml @@ -22,4 +22,4 @@ version: 0.0.2 # incremented each time you make changes to the application. Versions are not expected to # follow Semantic Versioning. They should reflect the version the application is using. # It is recommended to use it with quotes. -appVersion: "0.24.0" +appVersion: "0.25.0" diff --git a/helm/warpgate/values.yaml b/helm/warpgate/values.yaml index a41fa9fe4..78ebbb254 100644 --- a/helm/warpgate/values.yaml +++ b/helm/warpgate/values.yaml @@ -5,7 +5,7 @@ replicaCount: 1 image: repository: ghcr.io/warp-tech/warpgate pullPolicy: IfNotPresent - tag: "0.24.0" + tag: "0.25.0" # References to Kubernetes secrets for pulling images (if using a private registry) imagePullSecrets: [] diff --git a/warpgate-admin/Cargo.toml b/warpgate-admin/Cargo.toml index ce6fa1cc2..a4736f94a 100644 --- a/warpgate-admin/Cargo.toml +++ b/warpgate-admin/Cargo.toml @@ -2,7 +2,7 @@ edition = "2024" license = "Apache-2.0" name = "warpgate-admin" -version = "0.24.0" +version = "0.25.0" publish = false [dependencies] diff --git a/warpgate-ca/Cargo.toml b/warpgate-ca/Cargo.toml index 38c41c054..13f791cda 100644 --- a/warpgate-ca/Cargo.toml +++ b/warpgate-ca/Cargo.toml @@ -2,7 +2,7 @@ edition = "2024" license = "Apache-2.0" name = "warpgate-ca" -version = "0.24.0" +version = "0.25.0" publish = false [dependencies] diff --git a/warpgate-common-http/Cargo.toml b/warpgate-common-http/Cargo.toml index f7e7fe1be..035445a1c 100644 --- a/warpgate-common-http/Cargo.toml +++ b/warpgate-common-http/Cargo.toml @@ -2,7 +2,7 @@ edition = "2024" license = "Apache-2.0" name = "warpgate-common-http" -version = "0.24.0" +version = "0.25.0" publish = false [dependencies] diff --git a/warpgate-common/Cargo.toml b/warpgate-common/Cargo.toml index 8d3570d08..29d016da9 100644 --- a/warpgate-common/Cargo.toml +++ b/warpgate-common/Cargo.toml @@ -2,7 +2,7 @@ edition = "2024" license = "Apache-2.0" name = "warpgate-common" -version = "0.24.0" +version = "0.25.0" publish = false [[bin]] diff --git a/warpgate-core/Cargo.toml b/warpgate-core/Cargo.toml index bc7b4aa60..2c9b7a88a 100644 --- a/warpgate-core/Cargo.toml +++ b/warpgate-core/Cargo.toml @@ -2,7 +2,7 @@ edition = "2024" license = "Apache-2.0" name = "warpgate-core" -version = "0.24.0" +version = "0.25.0" publish = false [dependencies] diff --git a/warpgate-database-protocols/Cargo.toml b/warpgate-database-protocols/Cargo.toml index 4e2492c62..0cf121cc1 100644 --- a/warpgate-database-protocols/Cargo.toml +++ b/warpgate-database-protocols/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "warpgate-database-protocols" -version = "0.24.0" +version = "0.25.0" description = "Core of SQLx, the rust SQL toolkit. Just the database protocol parts." license = "MIT OR Apache-2.0" edition = "2024" diff --git a/warpgate-db-entities/Cargo.toml b/warpgate-db-entities/Cargo.toml index 999b4d8d7..e796ce6a9 100644 --- a/warpgate-db-entities/Cargo.toml +++ b/warpgate-db-entities/Cargo.toml @@ -2,7 +2,7 @@ edition = "2024" license = "Apache-2.0" name = "warpgate-db-entities" -version = "0.24.0" +version = "0.25.0" publish = false [dependencies] diff --git a/warpgate-db-migrations/Cargo.toml b/warpgate-db-migrations/Cargo.toml index f6043f166..03833f9f0 100644 --- a/warpgate-db-migrations/Cargo.toml +++ b/warpgate-db-migrations/Cargo.toml @@ -2,7 +2,7 @@ edition = "2024" license = "Apache-2.0" name = "warpgate-db-migrations" -version = "0.24.0" +version = "0.25.0" publish = false [lib] diff --git a/warpgate-ldap/Cargo.toml b/warpgate-ldap/Cargo.toml index e3062bb3d..e12c17512 100644 --- a/warpgate-ldap/Cargo.toml +++ b/warpgate-ldap/Cargo.toml @@ -2,7 +2,7 @@ edition = "2024" license = "Apache-2.0" name = "warpgate-ldap" -version = "0.24.0" +version = "0.25.0" publish = false [dependencies] diff --git a/warpgate-protocol-http/Cargo.toml b/warpgate-protocol-http/Cargo.toml index d642f11b9..0a514dca4 100644 --- a/warpgate-protocol-http/Cargo.toml +++ b/warpgate-protocol-http/Cargo.toml @@ -2,7 +2,7 @@ edition = "2024" license = "Apache-2.0" name = "warpgate-protocol-http" -version = "0.24.0" +version = "0.25.0" publish = false [dependencies] diff --git a/warpgate-protocol-kubernetes/Cargo.toml b/warpgate-protocol-kubernetes/Cargo.toml index 11258cbb5..db18628f1 100644 --- a/warpgate-protocol-kubernetes/Cargo.toml +++ b/warpgate-protocol-kubernetes/Cargo.toml @@ -2,7 +2,7 @@ edition = "2024" license = "Apache-2.0" name = "warpgate-protocol-kubernetes" -version = "0.24.0" +version = "0.25.0" publish = false [dependencies] diff --git a/warpgate-protocol-mysql/Cargo.toml b/warpgate-protocol-mysql/Cargo.toml index 6e67b744e..3134c47c7 100644 --- a/warpgate-protocol-mysql/Cargo.toml +++ b/warpgate-protocol-mysql/Cargo.toml @@ -2,7 +2,7 @@ edition = "2024" license = "Apache-2.0" name = "warpgate-protocol-mysql" -version = "0.24.0" +version = "0.25.0" publish = false [dependencies] diff --git a/warpgate-protocol-postgres/Cargo.toml b/warpgate-protocol-postgres/Cargo.toml index 8d6ac0f8e..f308d8f9d 100644 --- a/warpgate-protocol-postgres/Cargo.toml +++ b/warpgate-protocol-postgres/Cargo.toml @@ -2,7 +2,7 @@ edition = "2024" license = "Apache-2.0" name = "warpgate-protocol-postgres" -version = "0.24.0" +version = "0.25.0" publish = false [dependencies] diff --git a/warpgate-protocol-ssh/Cargo.toml b/warpgate-protocol-ssh/Cargo.toml index 10d39d242..a2556ed48 100644 --- a/warpgate-protocol-ssh/Cargo.toml +++ b/warpgate-protocol-ssh/Cargo.toml @@ -2,7 +2,7 @@ edition = "2024" license = "Apache-2.0" name = "warpgate-protocol-ssh" -version = "0.24.0" +version = "0.25.0" publish = false [dependencies] diff --git a/warpgate-sso/Cargo.toml b/warpgate-sso/Cargo.toml index 8cf5a348e..72fb12b9a 100644 --- a/warpgate-sso/Cargo.toml +++ b/warpgate-sso/Cargo.toml @@ -2,7 +2,7 @@ edition = "2024" license = "Apache-2.0" name = "warpgate-sso" -version = "0.24.0" +version = "0.25.0" publish = false [dependencies] diff --git a/warpgate-tls/Cargo.toml b/warpgate-tls/Cargo.toml index b9700caa6..694439afa 100644 --- a/warpgate-tls/Cargo.toml +++ b/warpgate-tls/Cargo.toml @@ -2,7 +2,7 @@ edition = "2024" license = "Apache-2.0" name = "warpgate-tls" -version = "0.24.0" +version = "0.25.0" publish = false [dependencies] diff --git a/warpgate-web/Cargo.toml b/warpgate-web/Cargo.toml index 1bc758063..c1890be1d 100644 --- a/warpgate-web/Cargo.toml +++ b/warpgate-web/Cargo.toml @@ -2,7 +2,7 @@ edition = "2024" license = "Apache-2.0" name = "warpgate-web" -version = "0.24.0" +version = "0.25.0" [dependencies] serde.workspace = true diff --git a/warpgate/Cargo.toml b/warpgate/Cargo.toml index 8d1f982f2..b55b03660 100644 --- a/warpgate/Cargo.toml +++ b/warpgate/Cargo.toml @@ -2,7 +2,7 @@ edition = "2024" license = "Apache-2.0" name = "warpgate" -version = "0.24.0" +version = "0.25.0" publish = false [dependencies] From 183c908b9b35b005f74a9063c446f98e95a6da76 Mon Sep 17 00:00:00 2001 From: Eugene Date: Sun, 7 Jun 2026 22:11:32 +0200 Subject: [PATCH 160/556] Update Cargo.lock --- Cargo.lock | 36 ++++++++++++++++++------------------ 1 file changed, 18 insertions(+), 18 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index c1f0d0134..4a3459d3e 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -7592,7 +7592,7 @@ dependencies = [ [[package]] name = "warpgate" -version = "0.24.0" +version = "0.25.0" dependencies = [ "anyhow", "async-trait", @@ -7635,7 +7635,7 @@ dependencies = [ [[package]] name = "warpgate-admin" -version = "0.24.0" +version = "0.25.0" dependencies = [ "anyhow", "async-trait", @@ -7695,7 +7695,7 @@ dependencies = [ [[package]] name = "warpgate-ca" -version = "0.24.0" +version = "0.25.0" dependencies = [ "aws-lc-rs", "bytes", @@ -7716,7 +7716,7 @@ dependencies = [ [[package]] name = "warpgate-common" -version = "0.24.0" +version = "0.25.0" dependencies = [ "anyhow", "argon2 0.5.3", @@ -7769,7 +7769,7 @@ dependencies = [ [[package]] name = "warpgate-common-http" -version = "0.24.0" +version = "0.25.0" dependencies = [ "poem", "poem-openapi", @@ -7784,7 +7784,7 @@ dependencies = [ [[package]] name = "warpgate-core" -version = "0.24.0" +version = "0.25.0" dependencies = [ "anyhow", "argon2 0.5.3", @@ -7829,7 +7829,7 @@ dependencies = [ [[package]] name = "warpgate-database-protocols" -version = "0.24.0" +version = "0.25.0" dependencies = [ "bitflags 2.13.0", "bytes", @@ -7842,7 +7842,7 @@ dependencies = [ [[package]] name = "warpgate-db-entities" -version = "0.24.0" +version = "0.25.0" dependencies = [ "bytes", "ipnet", @@ -7861,7 +7861,7 @@ dependencies = [ [[package]] name = "warpgate-db-migrations" -version = "0.24.0" +version = "0.25.0" dependencies = [ "data-encoding", "regex", @@ -7879,7 +7879,7 @@ dependencies = [ [[package]] name = "warpgate-ldap" -version = "0.24.0" +version = "0.25.0" dependencies = [ "anyhow", "ldap3", @@ -7895,7 +7895,7 @@ dependencies = [ [[package]] name = "warpgate-protocol-http" -version = "0.24.0" +version = "0.25.0" dependencies = [ "anyhow", "async-trait", @@ -7934,7 +7934,7 @@ dependencies = [ [[package]] name = "warpgate-protocol-kubernetes" -version = "0.24.0" +version = "0.25.0" dependencies = [ "anyhow", "async-trait", @@ -7973,7 +7973,7 @@ dependencies = [ [[package]] name = "warpgate-protocol-mysql" -version = "0.24.0" +version = "0.25.0" dependencies = [ "anyhow", "async-trait", @@ -8001,7 +8001,7 @@ dependencies = [ [[package]] name = "warpgate-protocol-postgres" -version = "0.24.0" +version = "0.25.0" dependencies = [ "anyhow", "async-trait", @@ -8028,7 +8028,7 @@ dependencies = [ [[package]] name = "warpgate-protocol-ssh" -version = "0.24.0" +version = "0.25.0" dependencies = [ "anyhow", "async-trait", @@ -8061,7 +8061,7 @@ dependencies = [ [[package]] name = "warpgate-sso" -version = "0.24.0" +version = "0.25.0" dependencies = [ "bytes", "data-encoding", @@ -8081,7 +8081,7 @@ dependencies = [ [[package]] name = "warpgate-tls" -version = "0.24.0" +version = "0.25.0" dependencies = [ "poem", "poem-openapi", @@ -8101,7 +8101,7 @@ dependencies = [ [[package]] name = "warpgate-web" -version = "0.24.0" +version = "0.25.0" dependencies = [ "rust-embed", "serde", From aa05af3d55142e4709b4a04bf08f09399cd23e8d Mon Sep 17 00:00:00 2001 From: Eugene Date: Mon, 8 Jun 2026 16:33:29 +0200 Subject: [PATCH 161/556] fixed #2025 - cannot create new SSH targets --- .../src/admin/config/targets/ssh/Options.svelte | 16 +++++++++++++++- 1 file changed, 15 insertions(+), 1 deletion(-) diff --git a/warpgate-web/src/admin/config/targets/ssh/Options.svelte b/warpgate-web/src/admin/config/targets/ssh/Options.svelte index a1c0d12ed..68eac5323 100644 --- a/warpgate-web/src/admin/config/targets/ssh/Options.svelte +++ b/warpgate-web/src/admin/config/targets/ssh/Options.svelte @@ -8,6 +8,7 @@ import { adminPermissions } from 'admin/lib/store' import { serverInfo } from 'gateway/lib/store' import { TargetKind } from 'gateway/lib/api' + import { untrack } from 'svelte' interface Props { id: string, @@ -28,6 +29,19 @@ api.getTargets().then(targets => { sshTargets = targets.filter(t => t.options.kind === TargetKind.Ssh && t.id !== id) }) + + let jumpHostSelectValue = $state('') + + $effect(() => { + const val = jumpHostSelectValue + untrack(() => { options.jumpHost = val || undefined }) + }) + + // Re-sync from options when the prop is reassigned (e.g. after save) + $effect(() => { + const jumpHost = options.jumpHost + untrack(() => { jumpHostSelectValue = jumpHost ?? '' }) + })

Connection

@@ -36,7 +50,7 @@ {#if sshTargets.length}
- {#each sshTargets as target (target.id)} From fb6047fffaae87282316d2490c5ee0c1451a7df7 Mon Sep 17 00:00:00 2001 From: Eugene Date: Mon, 8 Jun 2026 16:33:46 +0200 Subject: [PATCH 162/556] bump version to 0.25.1 --- bumpver.toml | 2 +- helm/warpgate/Chart.yaml | 2 +- helm/warpgate/values.yaml | 2 +- warpgate-admin/Cargo.toml | 2 +- warpgate-ca/Cargo.toml | 2 +- warpgate-common-http/Cargo.toml | 2 +- warpgate-common/Cargo.toml | 2 +- warpgate-core/Cargo.toml | 2 +- warpgate-database-protocols/Cargo.toml | 2 +- warpgate-db-entities/Cargo.toml | 2 +- warpgate-db-migrations/Cargo.toml | 2 +- warpgate-ldap/Cargo.toml | 2 +- warpgate-protocol-http/Cargo.toml | 2 +- warpgate-protocol-kubernetes/Cargo.toml | 2 +- warpgate-protocol-mysql/Cargo.toml | 2 +- warpgate-protocol-postgres/Cargo.toml | 2 +- warpgate-protocol-ssh/Cargo.toml | 2 +- warpgate-sso/Cargo.toml | 2 +- warpgate-tls/Cargo.toml | 2 +- warpgate-web/Cargo.toml | 2 +- warpgate/Cargo.toml | 2 +- 21 files changed, 21 insertions(+), 21 deletions(-) diff --git a/bumpver.toml b/bumpver.toml index f4fe74d93..982048b4a 100644 --- a/bumpver.toml +++ b/bumpver.toml @@ -1,5 +1,5 @@ [bumpver] -current_version = "0.25.0" +current_version = "0.25.1" version_pattern = "MAJOR.MINOR.PATCH[-TAG[.INC0]]" commit = true tag = false diff --git a/helm/warpgate/Chart.yaml b/helm/warpgate/Chart.yaml index c691eb829..1300d3d7f 100644 --- a/helm/warpgate/Chart.yaml +++ b/helm/warpgate/Chart.yaml @@ -22,4 +22,4 @@ version: 0.0.2 # incremented each time you make changes to the application. Versions are not expected to # follow Semantic Versioning. They should reflect the version the application is using. # It is recommended to use it with quotes. -appVersion: "0.25.0" +appVersion: "0.25.1" diff --git a/helm/warpgate/values.yaml b/helm/warpgate/values.yaml index 78ebbb254..fb0af086f 100644 --- a/helm/warpgate/values.yaml +++ b/helm/warpgate/values.yaml @@ -5,7 +5,7 @@ replicaCount: 1 image: repository: ghcr.io/warp-tech/warpgate pullPolicy: IfNotPresent - tag: "0.25.0" + tag: "0.25.1" # References to Kubernetes secrets for pulling images (if using a private registry) imagePullSecrets: [] diff --git a/warpgate-admin/Cargo.toml b/warpgate-admin/Cargo.toml index a4736f94a..e2c984e7f 100644 --- a/warpgate-admin/Cargo.toml +++ b/warpgate-admin/Cargo.toml @@ -2,7 +2,7 @@ edition = "2024" license = "Apache-2.0" name = "warpgate-admin" -version = "0.25.0" +version = "0.25.1" publish = false [dependencies] diff --git a/warpgate-ca/Cargo.toml b/warpgate-ca/Cargo.toml index 13f791cda..8fe24cc9d 100644 --- a/warpgate-ca/Cargo.toml +++ b/warpgate-ca/Cargo.toml @@ -2,7 +2,7 @@ edition = "2024" license = "Apache-2.0" name = "warpgate-ca" -version = "0.25.0" +version = "0.25.1" publish = false [dependencies] diff --git a/warpgate-common-http/Cargo.toml b/warpgate-common-http/Cargo.toml index 035445a1c..1d16d51f9 100644 --- a/warpgate-common-http/Cargo.toml +++ b/warpgate-common-http/Cargo.toml @@ -2,7 +2,7 @@ edition = "2024" license = "Apache-2.0" name = "warpgate-common-http" -version = "0.25.0" +version = "0.25.1" publish = false [dependencies] diff --git a/warpgate-common/Cargo.toml b/warpgate-common/Cargo.toml index 29d016da9..79100723b 100644 --- a/warpgate-common/Cargo.toml +++ b/warpgate-common/Cargo.toml @@ -2,7 +2,7 @@ edition = "2024" license = "Apache-2.0" name = "warpgate-common" -version = "0.25.0" +version = "0.25.1" publish = false [[bin]] diff --git a/warpgate-core/Cargo.toml b/warpgate-core/Cargo.toml index 2c9b7a88a..76b282390 100644 --- a/warpgate-core/Cargo.toml +++ b/warpgate-core/Cargo.toml @@ -2,7 +2,7 @@ edition = "2024" license = "Apache-2.0" name = "warpgate-core" -version = "0.25.0" +version = "0.25.1" publish = false [dependencies] diff --git a/warpgate-database-protocols/Cargo.toml b/warpgate-database-protocols/Cargo.toml index 0cf121cc1..c7f92b2f2 100644 --- a/warpgate-database-protocols/Cargo.toml +++ b/warpgate-database-protocols/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "warpgate-database-protocols" -version = "0.25.0" +version = "0.25.1" description = "Core of SQLx, the rust SQL toolkit. Just the database protocol parts." license = "MIT OR Apache-2.0" edition = "2024" diff --git a/warpgate-db-entities/Cargo.toml b/warpgate-db-entities/Cargo.toml index e796ce6a9..74bf2ba12 100644 --- a/warpgate-db-entities/Cargo.toml +++ b/warpgate-db-entities/Cargo.toml @@ -2,7 +2,7 @@ edition = "2024" license = "Apache-2.0" name = "warpgate-db-entities" -version = "0.25.0" +version = "0.25.1" publish = false [dependencies] diff --git a/warpgate-db-migrations/Cargo.toml b/warpgate-db-migrations/Cargo.toml index 03833f9f0..5f32d60f0 100644 --- a/warpgate-db-migrations/Cargo.toml +++ b/warpgate-db-migrations/Cargo.toml @@ -2,7 +2,7 @@ edition = "2024" license = "Apache-2.0" name = "warpgate-db-migrations" -version = "0.25.0" +version = "0.25.1" publish = false [lib] diff --git a/warpgate-ldap/Cargo.toml b/warpgate-ldap/Cargo.toml index e12c17512..175102c98 100644 --- a/warpgate-ldap/Cargo.toml +++ b/warpgate-ldap/Cargo.toml @@ -2,7 +2,7 @@ edition = "2024" license = "Apache-2.0" name = "warpgate-ldap" -version = "0.25.0" +version = "0.25.1" publish = false [dependencies] diff --git a/warpgate-protocol-http/Cargo.toml b/warpgate-protocol-http/Cargo.toml index 0a514dca4..437cb7ac7 100644 --- a/warpgate-protocol-http/Cargo.toml +++ b/warpgate-protocol-http/Cargo.toml @@ -2,7 +2,7 @@ edition = "2024" license = "Apache-2.0" name = "warpgate-protocol-http" -version = "0.25.0" +version = "0.25.1" publish = false [dependencies] diff --git a/warpgate-protocol-kubernetes/Cargo.toml b/warpgate-protocol-kubernetes/Cargo.toml index db18628f1..56c47d527 100644 --- a/warpgate-protocol-kubernetes/Cargo.toml +++ b/warpgate-protocol-kubernetes/Cargo.toml @@ -2,7 +2,7 @@ edition = "2024" license = "Apache-2.0" name = "warpgate-protocol-kubernetes" -version = "0.25.0" +version = "0.25.1" publish = false [dependencies] diff --git a/warpgate-protocol-mysql/Cargo.toml b/warpgate-protocol-mysql/Cargo.toml index 3134c47c7..1ad9175d5 100644 --- a/warpgate-protocol-mysql/Cargo.toml +++ b/warpgate-protocol-mysql/Cargo.toml @@ -2,7 +2,7 @@ edition = "2024" license = "Apache-2.0" name = "warpgate-protocol-mysql" -version = "0.25.0" +version = "0.25.1" publish = false [dependencies] diff --git a/warpgate-protocol-postgres/Cargo.toml b/warpgate-protocol-postgres/Cargo.toml index f308d8f9d..1a10e780b 100644 --- a/warpgate-protocol-postgres/Cargo.toml +++ b/warpgate-protocol-postgres/Cargo.toml @@ -2,7 +2,7 @@ edition = "2024" license = "Apache-2.0" name = "warpgate-protocol-postgres" -version = "0.25.0" +version = "0.25.1" publish = false [dependencies] diff --git a/warpgate-protocol-ssh/Cargo.toml b/warpgate-protocol-ssh/Cargo.toml index a2556ed48..12885a2a3 100644 --- a/warpgate-protocol-ssh/Cargo.toml +++ b/warpgate-protocol-ssh/Cargo.toml @@ -2,7 +2,7 @@ edition = "2024" license = "Apache-2.0" name = "warpgate-protocol-ssh" -version = "0.25.0" +version = "0.25.1" publish = false [dependencies] diff --git a/warpgate-sso/Cargo.toml b/warpgate-sso/Cargo.toml index 72fb12b9a..264cb99f1 100644 --- a/warpgate-sso/Cargo.toml +++ b/warpgate-sso/Cargo.toml @@ -2,7 +2,7 @@ edition = "2024" license = "Apache-2.0" name = "warpgate-sso" -version = "0.25.0" +version = "0.25.1" publish = false [dependencies] diff --git a/warpgate-tls/Cargo.toml b/warpgate-tls/Cargo.toml index 694439afa..bc851030e 100644 --- a/warpgate-tls/Cargo.toml +++ b/warpgate-tls/Cargo.toml @@ -2,7 +2,7 @@ edition = "2024" license = "Apache-2.0" name = "warpgate-tls" -version = "0.25.0" +version = "0.25.1" publish = false [dependencies] diff --git a/warpgate-web/Cargo.toml b/warpgate-web/Cargo.toml index c1890be1d..38cca2dde 100644 --- a/warpgate-web/Cargo.toml +++ b/warpgate-web/Cargo.toml @@ -2,7 +2,7 @@ edition = "2024" license = "Apache-2.0" name = "warpgate-web" -version = "0.25.0" +version = "0.25.1" [dependencies] serde.workspace = true diff --git a/warpgate/Cargo.toml b/warpgate/Cargo.toml index b55b03660..f516d5a70 100644 --- a/warpgate/Cargo.toml +++ b/warpgate/Cargo.toml @@ -2,7 +2,7 @@ edition = "2024" license = "Apache-2.0" name = "warpgate" -version = "0.25.0" +version = "0.25.1" publish = false [dependencies] From fce93113f5204e6f162e6f1ec90661238acbcb5e Mon Sep 17 00:00:00 2001 From: Eugene Date: Mon, 8 Jun 2026 16:33:53 +0200 Subject: [PATCH 163/556] Update Cargo.lock --- Cargo.lock | 36 ++++++++++++++++++------------------ 1 file changed, 18 insertions(+), 18 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index 4a3459d3e..745808503 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -7592,7 +7592,7 @@ dependencies = [ [[package]] name = "warpgate" -version = "0.25.0" +version = "0.25.1" dependencies = [ "anyhow", "async-trait", @@ -7635,7 +7635,7 @@ dependencies = [ [[package]] name = "warpgate-admin" -version = "0.25.0" +version = "0.25.1" dependencies = [ "anyhow", "async-trait", @@ -7695,7 +7695,7 @@ dependencies = [ [[package]] name = "warpgate-ca" -version = "0.25.0" +version = "0.25.1" dependencies = [ "aws-lc-rs", "bytes", @@ -7716,7 +7716,7 @@ dependencies = [ [[package]] name = "warpgate-common" -version = "0.25.0" +version = "0.25.1" dependencies = [ "anyhow", "argon2 0.5.3", @@ -7769,7 +7769,7 @@ dependencies = [ [[package]] name = "warpgate-common-http" -version = "0.25.0" +version = "0.25.1" dependencies = [ "poem", "poem-openapi", @@ -7784,7 +7784,7 @@ dependencies = [ [[package]] name = "warpgate-core" -version = "0.25.0" +version = "0.25.1" dependencies = [ "anyhow", "argon2 0.5.3", @@ -7829,7 +7829,7 @@ dependencies = [ [[package]] name = "warpgate-database-protocols" -version = "0.25.0" +version = "0.25.1" dependencies = [ "bitflags 2.13.0", "bytes", @@ -7842,7 +7842,7 @@ dependencies = [ [[package]] name = "warpgate-db-entities" -version = "0.25.0" +version = "0.25.1" dependencies = [ "bytes", "ipnet", @@ -7861,7 +7861,7 @@ dependencies = [ [[package]] name = "warpgate-db-migrations" -version = "0.25.0" +version = "0.25.1" dependencies = [ "data-encoding", "regex", @@ -7879,7 +7879,7 @@ dependencies = [ [[package]] name = "warpgate-ldap" -version = "0.25.0" +version = "0.25.1" dependencies = [ "anyhow", "ldap3", @@ -7895,7 +7895,7 @@ dependencies = [ [[package]] name = "warpgate-protocol-http" -version = "0.25.0" +version = "0.25.1" dependencies = [ "anyhow", "async-trait", @@ -7934,7 +7934,7 @@ dependencies = [ [[package]] name = "warpgate-protocol-kubernetes" -version = "0.25.0" +version = "0.25.1" dependencies = [ "anyhow", "async-trait", @@ -7973,7 +7973,7 @@ dependencies = [ [[package]] name = "warpgate-protocol-mysql" -version = "0.25.0" +version = "0.25.1" dependencies = [ "anyhow", "async-trait", @@ -8001,7 +8001,7 @@ dependencies = [ [[package]] name = "warpgate-protocol-postgres" -version = "0.25.0" +version = "0.25.1" dependencies = [ "anyhow", "async-trait", @@ -8028,7 +8028,7 @@ dependencies = [ [[package]] name = "warpgate-protocol-ssh" -version = "0.25.0" +version = "0.25.1" dependencies = [ "anyhow", "async-trait", @@ -8061,7 +8061,7 @@ dependencies = [ [[package]] name = "warpgate-sso" -version = "0.25.0" +version = "0.25.1" dependencies = [ "bytes", "data-encoding", @@ -8081,7 +8081,7 @@ dependencies = [ [[package]] name = "warpgate-tls" -version = "0.25.0" +version = "0.25.1" dependencies = [ "poem", "poem-openapi", @@ -8101,7 +8101,7 @@ dependencies = [ [[package]] name = "warpgate-web" -version = "0.25.0" +version = "0.25.1" dependencies = [ "rust-embed", "serde", From 86f0a07b495c1e471b1b5a314772aa3afb475996 Mon Sep 17 00:00:00 2001 From: Eugene Date: Tue, 9 Jun 2026 20:06:21 +0200 Subject: [PATCH 164/556] fixed #979 - fix target menu display in warp.dev --- warpgate-protocol-ssh/src/server/session.rs | 14 ++++++++++---- 1 file changed, 10 insertions(+), 4 deletions(-) diff --git a/warpgate-protocol-ssh/src/server/session.rs b/warpgate-protocol-ssh/src/server/session.rs index 078be962c..d3ef9c5af 100644 --- a/warpgate-protocol-ssh/src/server/session.rs +++ b/warpgate-protocol-ssh/src/server/session.rs @@ -561,6 +561,14 @@ impl ServerSession { Ok(()) } + async fn maybe_start_target_selection_menu(&self, channel_id: Uuid) -> Result<()> { + if matches!(self.target, TargetSelection::Menu) && self.pty_channels.contains(&channel_id) { + self.start_target_selection_menu(channel_id).await?; + } + + Ok(()) + } + async fn handle_server_handler_event(&mut self, event: ServerHandlerEvent) -> Result<()> { match event { ServerHandlerEvent::Authenticated(handle) => { @@ -637,10 +645,7 @@ impl ServerSession { ServerHandlerEvent::ShellRequest(server_channel_id, reply) => { let channel_id = self.map_channel(server_channel_id)?; self.maybe_connect_remote().await?; - - if matches!(self.target, TargetSelection::Menu) { - self.start_target_selection_menu(channel_id).await?; - } + self.maybe_start_target_selection_menu(channel_id).await?; let _ = self.send_command(RCCommand::Channel( channel_id, @@ -1306,6 +1311,7 @@ impl ServerSession { let is_scp = command == "scp" || command.starts_with("scp "); let _ = self.maybe_connect_remote().await; + self.maybe_start_target_selection_menu(channel_id).await?; let _ = self.send_command(RCCommand::Channel( channel_id, ChannelOperation::RequestExec(command.to_string()), From c437205e8dfe935dfee5dc125ea88ba3ff07b777 Mon Sep 17 00:00:00 2001 From: Eugene Date: Tue, 9 Jun 2026 21:09:31 +0200 Subject: [PATCH 165/556] fixed #2029 - broken MySQL query, remove log entry size limit (#2031) --- warpgate-core/src/config_providers/db.rs | 4 +- warpgate-db-entities/src/LogEntry.rs | 1 + warpgate-db-migrations/src/lib.rs | 2 + .../src/m00052_log_text_column.rs | 38 +++++++++++++++++++ 4 files changed, 44 insertions(+), 1 deletion(-) create mode 100644 warpgate-db-migrations/src/m00052_log_text_column.rs diff --git a/warpgate-core/src/config_providers/db.rs b/warpgate-core/src/config_providers/db.rs index 5362426a1..4aeed93be 100644 --- a/warpgate-core/src/config_providers/db.rs +++ b/warpgate-core/src/config_providers/db.rs @@ -279,7 +279,9 @@ impl ConfigProvider for DatabaseConfigProvider { let db: tokio::sync::MutexGuard<'_, DatabaseConnection> = self.db.lock().await; let hostname_query = match db.get_database_backend() { - DatabaseBackend::MySql => Expr::cust("options->>'$.http.external_host'"), + DatabaseBackend::MySql => { + Expr::cust("JSON_UNQUOTE(JSON_EXTRACT(options, '$.http.external_host'))") + } DatabaseBackend::Postgres => Expr::cust(r"options->'http'->>'external_host'"), DatabaseBackend::Sqlite => Expr::cust(r"json_extract(options, '$.http.external_host')"), }; diff --git a/warpgate-db-entities/src/LogEntry.rs b/warpgate-db-entities/src/LogEntry.rs index e45d271c1..b8e3d3ea4 100644 --- a/warpgate-db-entities/src/LogEntry.rs +++ b/warpgate-db-entities/src/LogEntry.rs @@ -11,6 +11,7 @@ use uuid::Uuid; pub struct Model { #[sea_orm(primary_key, auto_increment = false)] pub id: Uuid, + #[sea_orm(column_type = "Text")] pub text: String, pub target: String, pub values: JsonValue, diff --git a/warpgate-db-migrations/src/lib.rs b/warpgate-db-migrations/src/lib.rs index bd18647b0..c661d725b 100644 --- a/warpgate-db-migrations/src/lib.rs +++ b/warpgate-db-migrations/src/lib.rs @@ -53,6 +53,7 @@ mod m00048_target_click_action; mod m00049_text_columns; mod m00050_password_policy; mod m00051_tutorial_dismissed; +mod m00052_log_text_column; pub(crate) mod helpers; @@ -113,6 +114,7 @@ impl MigratorTrait for Migrator { Box::new(m00049_text_columns::Migration), Box::new(m00050_password_policy::Migration), Box::new(m00051_tutorial_dismissed::Migration), + Box::new(m00052_log_text_column::Migration), ] } } diff --git a/warpgate-db-migrations/src/m00052_log_text_column.rs b/warpgate-db-migrations/src/m00052_log_text_column.rs new file mode 100644 index 000000000..7bc30ef51 --- /dev/null +++ b/warpgate-db-migrations/src/m00052_log_text_column.rs @@ -0,0 +1,38 @@ +use sea_orm::DbBackend; +use sea_orm_migration::prelude::*; + +#[derive(DeriveMigrationName)] +pub struct Migration; + +#[async_trait::async_trait] +impl MigrationTrait for Migration { + async fn up(&self, manager: &SchemaManager) -> Result<(), DbErr> { + let connection = manager.get_connection(); + if connection.get_database_backend() != DbBackend::Sqlite { + manager + .alter_table( + Table::alter() + .table(Alias::new("log")) + .modify_column(ColumnDef::new(Alias::new("text")).text().not_null()) + .to_owned(), + ) + .await?; + } + Ok(()) + } + + async fn down(&self, manager: &SchemaManager) -> Result<(), DbErr> { + let connection = manager.get_connection(); + if connection.get_database_backend() != DbBackend::Sqlite { + manager + .alter_table( + Table::alter() + .table(Alias::new("log")) + .modify_column(ColumnDef::new(Alias::new("text")).string().not_null()) + .to_owned(), + ) + .await?; + } + Ok(()) + } +} From f6ec5513a01ca7d9ff23324ab5f1b5f7ea690fe7 Mon Sep 17 00:00:00 2001 From: Eugene Date: Tue, 9 Jun 2026 23:32:37 +0200 Subject: [PATCH 166/556] fixed #2027 - make username matching case insensitive (#2032) --- warpgate-admin/src/api/common.rs | 2 +- warpgate-admin/src/api/ldap_servers.rs | 4 +++- warpgate-admin/src/api/sessions_list.rs | 7 ++++++- warpgate-admin/src/api/tickets_list.rs | 2 +- warpgate-core/src/auth_state_store.rs | 2 +- warpgate-core/src/config_providers/db.rs | 12 ++++++------ warpgate-db-entities/src/User.rs | 7 +++++++ warpgate-protocol-http/src/api/common.rs | 4 ++-- warpgate-protocol-http/src/api/info.rs | 10 +++++----- warpgate-protocol-http/src/common.rs | 2 +- warpgate/src/commands/create_user.rs | 2 +- warpgate/src/commands/recover_access.rs | 2 +- 12 files changed, 35 insertions(+), 21 deletions(-) diff --git a/warpgate-admin/src/api/common.rs b/warpgate-admin/src/api/common.rs index 6be932863..b1e0e68e2 100644 --- a/warpgate-admin/src/api/common.rs +++ b/warpgate-admin/src/api/common.rs @@ -30,7 +30,7 @@ pub async fn has_admin_permission( let db = ctx.services().db.lock().await; let Some(user_model) = User::Entity::find() - .filter(User::Column::Username.eq(username)) + .filter(User::Entity::username_eq_ci(username)) .one(&*db) .await? else { diff --git a/warpgate-admin/src/api/ldap_servers.rs b/warpgate-admin/src/api/ldap_servers.rs index f3dcd5a41..228b9b543 100644 --- a/warpgate-admin/src/api/ldap_servers.rs +++ b/warpgate-admin/src/api/ldap_servers.rs @@ -63,7 +63,9 @@ impl ImportApi { for dn in &body.dns { if let Some(user) = all_users.iter().find(|u| &u.dn == dn) { let existing = warpgate_db_entities::User::Entity::find() - .filter(warpgate_db_entities::User::Column::Username.eq(&user.username)) + .filter(warpgate_db_entities::User::Entity::username_eq_ci( + &user.username, + )) .one(&*db) .await?; if existing.is_none() { diff --git a/warpgate-admin/src/api/sessions_list.rs b/warpgate-admin/src/api/sessions_list.rs index 1fe7b41cd..b7c257ef3 100644 --- a/warpgate-admin/src/api/sessions_list.rs +++ b/warpgate-admin/src/api/sessions_list.rs @@ -6,6 +6,8 @@ use poem::{IntoResponse, handler}; use poem_openapi::param::Query; use poem_openapi::payload::Json; use poem_openapi::{ApiResponse, OpenApi}; +use sea_orm::prelude::Expr; +use sea_orm::sea_query::Func; use sea_orm::{ColumnTrait, EntityTrait, QueryFilter, QueryOrder}; use warpgate_common::{AdminPermission, WarpgateError}; use warpgate_common_http::AuthenticatedRequestContext; @@ -58,7 +60,10 @@ impl Api { q = q.filter(Session::Column::Username.is_not_null()); } if let Some(username_filter) = username.as_ref() { - q = q.filter(Session::Column::Username.eq(username_filter.as_str())); + q = q.filter( + Expr::expr(Func::lower(Expr::col(Session::Column::Username))) + .eq(username_filter.to_lowercase()), + ); } Ok(GetSessionsResponse::Ok(Json( diff --git a/warpgate-admin/src/api/tickets_list.rs b/warpgate-admin/src/api/tickets_list.rs index 29152b9df..4c66feaf7 100644 --- a/warpgate-admin/src/api/tickets_list.rs +++ b/warpgate-admin/src/api/tickets_list.rs @@ -140,7 +140,7 @@ impl Api { User::Entity::find_by_id(user_id).one(&*db).await? } else if let Some(username) = &body.username { User::Entity::find() - .filter(User::Column::Username.eq(username.clone())) + .filter(User::Entity::username_eq_ci(username)) .one(&*db) .await? } else { diff --git a/warpgate-core/src/auth_state_store.rs b/warpgate-core/src/auth_state_store.rs index 4e582185f..022a1a359 100644 --- a/warpgate-core/src/auth_state_store.rs +++ b/warpgate-core/src/auth_state_store.rs @@ -139,7 +139,7 @@ impl AuthStateStore { .list_users() .await? .iter() - .find(|u| u.username == username) + .find(|u| u.username.to_lowercase() == username.to_lowercase()) .cloned() else { return Err(WarpgateError::UserNotFound(username.into())); diff --git a/warpgate-core/src/config_providers/db.rs b/warpgate-core/src/config_providers/db.rs index 4aeed93be..a5bf8957d 100644 --- a/warpgate-core/src/config_providers/db.rs +++ b/warpgate-core/src/config_providers/db.rs @@ -168,7 +168,7 @@ impl DatabaseConfigProvider { } let existing_user = entities::User::Entity::find() - .filter(entities::User::Column::Username.eq(&preferred_username)) + .filter(entities::User::Entity::username_eq_ci(&preferred_username)) .one(db) .await?; @@ -305,7 +305,7 @@ impl ConfigProvider for DatabaseConfigProvider { let db = self.db.lock().await; let user_model = entities::User::Entity::find() - .filter(entities::User::Column::Username.eq(username)) + .filter(entities::User::Entity::username_eq_ci(username)) .one(&*db) .await?; @@ -460,7 +460,7 @@ impl ConfigProvider for DatabaseConfigProvider { let db = self.db.lock().await; let user_model = entities::User::Entity::find() - .filter(entities::User::Column::Username.eq(username)) + .filter(entities::User::Entity::username_eq_ci(username)) .one(&*db) .await?; @@ -571,7 +571,7 @@ impl ConfigProvider for DatabaseConfigProvider { .await?; let user_model = entities::User::Entity::find() - .filter(entities::User::Column::Username.eq(username)) + .filter(entities::User::Entity::username_eq_ci(username)) .one(&*db) .await?; @@ -624,7 +624,7 @@ impl ConfigProvider for DatabaseConfigProvider { let db = self.db.lock().await; let user = entities::User::Entity::find() - .filter(entities::User::Column::Username.eq(username)) + .filter(entities::User::Entity::username_eq_ci(username)) .one(&*db) .await? .ok_or_else(|| WarpgateError::UserNotFound(username.into()))?; @@ -685,7 +685,7 @@ impl ConfigProvider for DatabaseConfigProvider { let db = self.db.lock().await; let user = entities::User::Entity::find() - .filter(entities::User::Column::Username.eq(username)) + .filter(entities::User::Entity::username_eq_ci(username)) .one(&*db) .await? .ok_or_else(|| WarpgateError::UserNotFound(username.into()))?; diff --git a/warpgate-db-entities/src/User.rs b/warpgate-db-entities/src/User.rs index 47048f07d..4159922a4 100644 --- a/warpgate-db-entities/src/User.rs +++ b/warpgate-db-entities/src/User.rs @@ -4,6 +4,7 @@ use ipnet::IpNet; use poem_openapi::Object; use sea_orm::Set; use sea_orm::entity::prelude::*; +use sea_orm::sea_query::{Func, IntoCondition}; use serde::Serialize; use uuid::Uuid; use warpgate_common::{User, UserDetails, WarpgateError}; @@ -137,6 +138,12 @@ impl RelationTrait for Relation { impl ActiveModelBehavior for ActiveModel {} +impl Entity { + pub fn username_eq_ci(username: &str) -> impl IntoCondition { + Expr::expr(Func::lower(Expr::col(Column::Username))).eq(username.to_lowercase()) + } +} + impl TryFrom for User { type Error = WarpgateError; diff --git a/warpgate-protocol-http/src/api/common.rs b/warpgate-protocol-http/src/api/common.rs index 119c144f5..f00032d5f 100644 --- a/warpgate-protocol-http/src/api/common.rs +++ b/warpgate-protocol-http/src/api/common.rs @@ -1,5 +1,5 @@ use poem::session::Session; -use sea_orm::{ColumnTrait, DatabaseConnection, EntityTrait, QueryFilter}; +use sea_orm::{DatabaseConnection, EntityTrait, QueryFilter}; use tracing::info; use warpgate_common::{SessionId, WarpgateError}; use warpgate_common_http::RequestAuthorization; @@ -43,7 +43,7 @@ pub async fn get_user( }; let Some(user_model) = entities::User::Entity::find() - .filter(entities::User::Column::Username.eq(username)) + .filter(entities::User::Entity::username_eq_ci(username)) .one(db) .await? else { diff --git a/warpgate-protocol-http/src/api/info.rs b/warpgate-protocol-http/src/api/info.rs index e3375ae65..e2f94be33 100644 --- a/warpgate-protocol-http/src/api/info.rs +++ b/warpgate-protocol-http/src/api/info.rs @@ -4,7 +4,7 @@ use poem::session::Session; use poem::web::Data; use poem_openapi::payload::Json; use poem_openapi::{ApiResponse, Object, OpenApi}; -use sea_orm::{ColumnTrait, EntityTrait, IntoActiveModel, ModelTrait, QueryFilter, Set}; +use sea_orm::{EntityTrait, IntoActiveModel, ModelTrait, QueryFilter, Set}; use serde::Serialize; use warpgate_common::version::warpgate_version; use warpgate_common_http::auth::UnauthenticatedRequestContext; @@ -43,7 +43,7 @@ pub struct SetupState { } impl SetupState { - pub fn completed(&self) -> bool { + pub const fn completed(&self) -> bool { self.tutorial_dismissed || (self.has_targets && self.has_users) } } @@ -153,7 +153,7 @@ impl Api { }; if user_is_admin { let state = SetupState { - has_targets: targets.len() > 0, + has_targets: !targets.is_empty(), has_users: users.len() > 1, tutorial_dismissed: parameters.tutorial_dismissed, }; @@ -215,7 +215,7 @@ impl Api { let perms = { let mut combined = AdminPermissions::default(); if let Some(user) = User::Entity::find() - .filter(User::Column::Username.eq(username)) + .filter(User::Entity::username_eq_ci(username)) .one(&*db) .await .context("loading user")? @@ -345,7 +345,7 @@ impl Api { } let db = ctx.services().db.lock().await; - let mut parameters = Parameters::Entity::get(&*db) + let mut parameters = Parameters::Entity::get(&db) .await .context("loading parameters")? .into_active_model(); diff --git a/warpgate-protocol-http/src/common.rs b/warpgate-protocol-http/src/common.rs index 5e3410ba8..5d13a2308 100644 --- a/warpgate-protocol-http/src/common.rs +++ b/warpgate-protocol-http/src/common.rs @@ -122,7 +122,7 @@ pub async fn is_user_admin(ctx: &AuthenticatedRequestContext) -> poem::Result) -> Result let user = match username { Some(username) => users .iter_mut() - .find(|x| &x.username == username) + .find(|x| x.username.to_lowercase() == username.to_lowercase()) .ok_or_else(|| anyhow::anyhow!("User not found"))?, None => { From 3e2a3b0ccbe059f70f0193773595f92c94d69c0c Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Tue, 9 Jun 2026 23:38:07 +0200 Subject: [PATCH 167/556] Bump x509-parser from 0.17.0 to 0.18.1 (#1934) Signed-off-by: dependabot[bot] --- Cargo.lock | 27 +++++---------------------- Cargo.toml | 2 +- warpgate-common/Cargo.toml | 2 +- warpgate-tls/Cargo.toml | 2 +- 4 files changed, 8 insertions(+), 25 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index 745808503..3a51fbbf3 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -3473,7 +3473,7 @@ dependencies = [ "tokio-stream", "tokio-util", "url", - "x509-parser 0.18.1", + "x509-parser", ] [[package]] @@ -5087,7 +5087,7 @@ dependencies = [ "pem", "rustls-pki-types", "time", - "x509-parser 0.18.1", + "x509-parser", "yasna", "zeroize", ] @@ -7711,7 +7711,7 @@ dependencies = [ "tracing", "uuid", "x509-cert", - "x509-parser 0.17.0", + "x509-parser", ] [[package]] @@ -7764,7 +7764,7 @@ dependencies = [ "warpgate-sso", "warpgate-tls", "webpki", - "x509-parser 0.17.0", + "x509-parser", ] [[package]] @@ -8096,7 +8096,7 @@ dependencies = [ "tokio-rustls 0.26.4", "tracing", "webpki", - "x509-parser 0.17.0", + "x509-parser", ] [[package]] @@ -8938,23 +8938,6 @@ dependencies = [ "zeroize", ] -[[package]] -name = "x509-parser" -version = "0.17.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "4569f339c0c402346d4a75a9e39cf8dad310e287eef1ff56d4c68e5067f53460" -dependencies = [ - "asn1-rs", - "data-encoding", - "der-parser", - "lazy_static", - "nom", - "oid-registry", - "rusticata-macros", - "thiserror 2.0.18", - "time", -] - [[package]] name = "x509-parser" version = "0.18.1" diff --git a/Cargo.toml b/Cargo.toml index 214d97fd0..c17165174 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -75,7 +75,7 @@ dialoguer = { version = "0.12", default-features = false, features = ["editor", tokio = { version = "1.52", features = ["tracing", "signal", "macros", "rt-multi-thread", "io-util"], default-features = false } governor = { version = "0.10.0", default-features = false, features = ["std", "quanta", "jitter"] } rcgen = { version = "0.14", features = ["zeroize", "crypto", "aws_lc_rs", "pem", "x509-parser"], default-features = false } -x509-parser = "0.17.0" +x509-parser = "0.18.1" uuid = { version = "1.23", features = ["v4", "serde"], default-features = false } reqwest = { version = "0.13", features = [ "http2", # required for connecting to targets behind AWS ELB diff --git a/warpgate-common/Cargo.toml b/warpgate-common/Cargo.toml index 79100723b..f87f4a3d7 100644 --- a/warpgate-common/Cargo.toml +++ b/warpgate-common/Cargo.toml @@ -57,4 +57,4 @@ warpgate-ldap = { path = "../warpgate-ldap" } warpgate-sso = { path = "../warpgate-sso", default-features = false } warpgate-tls = { path = "../warpgate-tls", default-features = false } webpki = { version = "0.22", default-features = false } -x509-parser = "0.17.0" +x509-parser = "0.18.1" diff --git a/warpgate-tls/Cargo.toml b/warpgate-tls/Cargo.toml index bc851030e..887c8fa3f 100644 --- a/warpgate-tls/Cargo.toml +++ b/warpgate-tls/Cargo.toml @@ -19,4 +19,4 @@ tokio-rustls.workspace = true tokio.workspace = true tracing.workspace = true webpki = { version = "0.22", default-features = false } -x509-parser = "0.17.0" +x509-parser = "0.18.1" From 03d119b03e593d3ad2e5095e629335c0e081d261 Mon Sep 17 00:00:00 2001 From: Eugene Date: Tue, 9 Jun 2026 23:50:24 +0200 Subject: [PATCH 168/556] move to stable rsasl --- Cargo.lock | 5 +++-- warpgate-protocol-postgres/Cargo.toml | 2 +- 2 files changed, 4 insertions(+), 3 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index 3a51fbbf3..30a6830c3 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -5359,8 +5359,9 @@ dependencies = [ [[package]] name = "rsasl" -version = "2.2.1" -source = "git+https://github.com/Eugeny/rsasl.git?rev=cbf4b0f3e05c73b33ac332718ab6e955eaa58166#cbf4b0f3e05c73b33ac332718ab6e955eaa58166" +version = "2.3.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ed828a88913fd477c73bc3768b05d4b335ee775e29f2397bb59b9a4dd69ffb83" dependencies = [ "base64 0.22.1", "digest 0.10.7", diff --git a/warpgate-protocol-postgres/Cargo.toml b/warpgate-protocol-postgres/Cargo.toml index 1a10e780b..93ff94043 100644 --- a/warpgate-protocol-postgres/Cargo.toml +++ b/warpgate-protocol-postgres/Cargo.toml @@ -25,7 +25,7 @@ rustls-native-certs = { version = "0.8", default-features = false } pgwire = { version = "0.40", default-features = false, features = [ "server-api-aws-lc-rs", ] } -rsasl = { git = "https://github.com/Eugeny/rsasl.git", rev = "cbf4b0f3e05c73b33ac332718ab6e955eaa58166", default-features = false, features = [ +rsasl = { version = "2.3", default-features = false, features = [ "config_builder", "scram-sha-2", "std", From 67b5d258ae6db5f6aa6425f6a2bae87abe5cb183 Mon Sep 17 00:00:00 2001 From: Eugene Date: Tue, 9 Jun 2026 23:50:27 +0200 Subject: [PATCH 169/556] Update deny.toml --- deny.toml | 15 ++++----------- 1 file changed, 4 insertions(+), 11 deletions(-) diff --git a/deny.toml b/deny.toml index ca39fbcb4..eaf5f4a3d 100644 --- a/deny.toml +++ b/deny.toml @@ -39,7 +39,7 @@ targets = [ # If true, metadata will be collected with `--all-features`. Note that this can't # be toggled off if true, if you want to conditionally enable `--all-features` it # is recommended to pass `--all-features` on the cmd line instead -all-features = false +all-features = true # If true, metadata will be collected with `--no-default-features`. The same # caveat with `all-features` applies no-default-features = false @@ -74,6 +74,7 @@ ignore = [ "RUSTSEC-2026-0099", # rustls: Name constraints were accepted for certificates asserting a wildcard name "RUSTSEC-2026-0098", # rustls: Name constraints for URI names were incorrectly accepted "RUSTSEC-2026-0104", # an older pinned version of rustls-webpki used by AWS SDK + "RUSTSEC-2026-0173", # proc-macro-error2 is unmaintained - waiting for a sea-orm update ] # If this is true, then cargo deny will use the git executable to fetch advisory database. # If this is false, then it uses a built-in git library. @@ -87,9 +88,10 @@ ignore = [ # https://embarkstudios.github.io/cargo-deny/checks/bans/cfg.html [bans] # Lint level for when multiple versions of the same crate are detected -# multiple-versions = "deny" +multiple-versions = "allow" # Lint level for when a crate version requirement is `*` wildcards = "warn" +allow-wildcard-paths = true # The graph highlighting used when creating dotgraphs for crates # with multiple versions # * lowest-version - The path to the lowest versioned duplicate is highlighted @@ -125,12 +127,6 @@ deny = [ # # Features to not allow # deny = ["ring"] -[[bans.features]] -crate = "reqwest" -# Features to not allow -deny = ["rustls-tls-webpki-roots"] - - # Features to allow #allow = [ @@ -177,7 +173,6 @@ unknown-git = "deny" allow-registry = ["https://github.com/rust-lang/crates.io-index"] # List of URLs for allowed Git repositories allow-git = [ - "git+https://github.com/Eugeny/rsasl.git?rev=cbf4b0f3e05c73b33ac332718ab6e955eaa58166", ] [sources.allow-org] @@ -196,13 +191,11 @@ allow = [ "Unicode-3.0", "Unicode-DFS-2016", "ISC", - "OpenSSL", "BSD-2-Clause", "BSD-3-Clause", "Zlib", "WTFPL", "CC0-1.0", - "LGPL-3.0", "MPL-2.0", "CDLA-Permissive-2.0", ] From 7812fb39b33d0c196c66d3ef197f4c37aedd37de Mon Sep 17 00:00:00 2001 From: Eugene Date: Tue, 9 Jun 2026 23:50:32 +0200 Subject: [PATCH 170/556] bump version to 0.25.2 --- bumpver.toml | 2 +- helm/warpgate/Chart.yaml | 2 +- helm/warpgate/values.yaml | 2 +- warpgate-admin/Cargo.toml | 2 +- warpgate-ca/Cargo.toml | 2 +- warpgate-common-http/Cargo.toml | 2 +- warpgate-common/Cargo.toml | 2 +- warpgate-core/Cargo.toml | 2 +- warpgate-database-protocols/Cargo.toml | 2 +- warpgate-db-entities/Cargo.toml | 2 +- warpgate-db-migrations/Cargo.toml | 2 +- warpgate-ldap/Cargo.toml | 2 +- warpgate-protocol-http/Cargo.toml | 2 +- warpgate-protocol-kubernetes/Cargo.toml | 2 +- warpgate-protocol-mysql/Cargo.toml | 2 +- warpgate-protocol-postgres/Cargo.toml | 2 +- warpgate-protocol-ssh/Cargo.toml | 2 +- warpgate-sso/Cargo.toml | 2 +- warpgate-tls/Cargo.toml | 2 +- warpgate-web/Cargo.toml | 2 +- warpgate/Cargo.toml | 2 +- 21 files changed, 21 insertions(+), 21 deletions(-) diff --git a/bumpver.toml b/bumpver.toml index 982048b4a..e147e9692 100644 --- a/bumpver.toml +++ b/bumpver.toml @@ -1,5 +1,5 @@ [bumpver] -current_version = "0.25.1" +current_version = "0.25.2" version_pattern = "MAJOR.MINOR.PATCH[-TAG[.INC0]]" commit = true tag = false diff --git a/helm/warpgate/Chart.yaml b/helm/warpgate/Chart.yaml index 1300d3d7f..92a5097a4 100644 --- a/helm/warpgate/Chart.yaml +++ b/helm/warpgate/Chart.yaml @@ -22,4 +22,4 @@ version: 0.0.2 # incremented each time you make changes to the application. Versions are not expected to # follow Semantic Versioning. They should reflect the version the application is using. # It is recommended to use it with quotes. -appVersion: "0.25.1" +appVersion: "0.25.2" diff --git a/helm/warpgate/values.yaml b/helm/warpgate/values.yaml index fb0af086f..d996a5142 100644 --- a/helm/warpgate/values.yaml +++ b/helm/warpgate/values.yaml @@ -5,7 +5,7 @@ replicaCount: 1 image: repository: ghcr.io/warp-tech/warpgate pullPolicy: IfNotPresent - tag: "0.25.1" + tag: "0.25.2" # References to Kubernetes secrets for pulling images (if using a private registry) imagePullSecrets: [] diff --git a/warpgate-admin/Cargo.toml b/warpgate-admin/Cargo.toml index e2c984e7f..3cdf8fa21 100644 --- a/warpgate-admin/Cargo.toml +++ b/warpgate-admin/Cargo.toml @@ -2,7 +2,7 @@ edition = "2024" license = "Apache-2.0" name = "warpgate-admin" -version = "0.25.1" +version = "0.25.2" publish = false [dependencies] diff --git a/warpgate-ca/Cargo.toml b/warpgate-ca/Cargo.toml index 8fe24cc9d..492d4e6ed 100644 --- a/warpgate-ca/Cargo.toml +++ b/warpgate-ca/Cargo.toml @@ -2,7 +2,7 @@ edition = "2024" license = "Apache-2.0" name = "warpgate-ca" -version = "0.25.1" +version = "0.25.2" publish = false [dependencies] diff --git a/warpgate-common-http/Cargo.toml b/warpgate-common-http/Cargo.toml index 1d16d51f9..dd27a721a 100644 --- a/warpgate-common-http/Cargo.toml +++ b/warpgate-common-http/Cargo.toml @@ -2,7 +2,7 @@ edition = "2024" license = "Apache-2.0" name = "warpgate-common-http" -version = "0.25.1" +version = "0.25.2" publish = false [dependencies] diff --git a/warpgate-common/Cargo.toml b/warpgate-common/Cargo.toml index f87f4a3d7..f0ae36998 100644 --- a/warpgate-common/Cargo.toml +++ b/warpgate-common/Cargo.toml @@ -2,7 +2,7 @@ edition = "2024" license = "Apache-2.0" name = "warpgate-common" -version = "0.25.1" +version = "0.25.2" publish = false [[bin]] diff --git a/warpgate-core/Cargo.toml b/warpgate-core/Cargo.toml index 76b282390..a15263b3d 100644 --- a/warpgate-core/Cargo.toml +++ b/warpgate-core/Cargo.toml @@ -2,7 +2,7 @@ edition = "2024" license = "Apache-2.0" name = "warpgate-core" -version = "0.25.1" +version = "0.25.2" publish = false [dependencies] diff --git a/warpgate-database-protocols/Cargo.toml b/warpgate-database-protocols/Cargo.toml index c7f92b2f2..1bec7e0aa 100644 --- a/warpgate-database-protocols/Cargo.toml +++ b/warpgate-database-protocols/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "warpgate-database-protocols" -version = "0.25.1" +version = "0.25.2" description = "Core of SQLx, the rust SQL toolkit. Just the database protocol parts." license = "MIT OR Apache-2.0" edition = "2024" diff --git a/warpgate-db-entities/Cargo.toml b/warpgate-db-entities/Cargo.toml index 74bf2ba12..67865ad51 100644 --- a/warpgate-db-entities/Cargo.toml +++ b/warpgate-db-entities/Cargo.toml @@ -2,7 +2,7 @@ edition = "2024" license = "Apache-2.0" name = "warpgate-db-entities" -version = "0.25.1" +version = "0.25.2" publish = false [dependencies] diff --git a/warpgate-db-migrations/Cargo.toml b/warpgate-db-migrations/Cargo.toml index 5f32d60f0..dc54b6f99 100644 --- a/warpgate-db-migrations/Cargo.toml +++ b/warpgate-db-migrations/Cargo.toml @@ -2,7 +2,7 @@ edition = "2024" license = "Apache-2.0" name = "warpgate-db-migrations" -version = "0.25.1" +version = "0.25.2" publish = false [lib] diff --git a/warpgate-ldap/Cargo.toml b/warpgate-ldap/Cargo.toml index 175102c98..2b9fe47cb 100644 --- a/warpgate-ldap/Cargo.toml +++ b/warpgate-ldap/Cargo.toml @@ -2,7 +2,7 @@ edition = "2024" license = "Apache-2.0" name = "warpgate-ldap" -version = "0.25.1" +version = "0.25.2" publish = false [dependencies] diff --git a/warpgate-protocol-http/Cargo.toml b/warpgate-protocol-http/Cargo.toml index 437cb7ac7..bd7c44535 100644 --- a/warpgate-protocol-http/Cargo.toml +++ b/warpgate-protocol-http/Cargo.toml @@ -2,7 +2,7 @@ edition = "2024" license = "Apache-2.0" name = "warpgate-protocol-http" -version = "0.25.1" +version = "0.25.2" publish = false [dependencies] diff --git a/warpgate-protocol-kubernetes/Cargo.toml b/warpgate-protocol-kubernetes/Cargo.toml index 56c47d527..838a8068b 100644 --- a/warpgate-protocol-kubernetes/Cargo.toml +++ b/warpgate-protocol-kubernetes/Cargo.toml @@ -2,7 +2,7 @@ edition = "2024" license = "Apache-2.0" name = "warpgate-protocol-kubernetes" -version = "0.25.1" +version = "0.25.2" publish = false [dependencies] diff --git a/warpgate-protocol-mysql/Cargo.toml b/warpgate-protocol-mysql/Cargo.toml index 1ad9175d5..13a545e3d 100644 --- a/warpgate-protocol-mysql/Cargo.toml +++ b/warpgate-protocol-mysql/Cargo.toml @@ -2,7 +2,7 @@ edition = "2024" license = "Apache-2.0" name = "warpgate-protocol-mysql" -version = "0.25.1" +version = "0.25.2" publish = false [dependencies] diff --git a/warpgate-protocol-postgres/Cargo.toml b/warpgate-protocol-postgres/Cargo.toml index 93ff94043..31c71a8c5 100644 --- a/warpgate-protocol-postgres/Cargo.toml +++ b/warpgate-protocol-postgres/Cargo.toml @@ -2,7 +2,7 @@ edition = "2024" license = "Apache-2.0" name = "warpgate-protocol-postgres" -version = "0.25.1" +version = "0.25.2" publish = false [dependencies] diff --git a/warpgate-protocol-ssh/Cargo.toml b/warpgate-protocol-ssh/Cargo.toml index 12885a2a3..266ebf60a 100644 --- a/warpgate-protocol-ssh/Cargo.toml +++ b/warpgate-protocol-ssh/Cargo.toml @@ -2,7 +2,7 @@ edition = "2024" license = "Apache-2.0" name = "warpgate-protocol-ssh" -version = "0.25.1" +version = "0.25.2" publish = false [dependencies] diff --git a/warpgate-sso/Cargo.toml b/warpgate-sso/Cargo.toml index 264cb99f1..75de58ebf 100644 --- a/warpgate-sso/Cargo.toml +++ b/warpgate-sso/Cargo.toml @@ -2,7 +2,7 @@ edition = "2024" license = "Apache-2.0" name = "warpgate-sso" -version = "0.25.1" +version = "0.25.2" publish = false [dependencies] diff --git a/warpgate-tls/Cargo.toml b/warpgate-tls/Cargo.toml index 887c8fa3f..4413f4516 100644 --- a/warpgate-tls/Cargo.toml +++ b/warpgate-tls/Cargo.toml @@ -2,7 +2,7 @@ edition = "2024" license = "Apache-2.0" name = "warpgate-tls" -version = "0.25.1" +version = "0.25.2" publish = false [dependencies] diff --git a/warpgate-web/Cargo.toml b/warpgate-web/Cargo.toml index 38cca2dde..91d167722 100644 --- a/warpgate-web/Cargo.toml +++ b/warpgate-web/Cargo.toml @@ -2,7 +2,7 @@ edition = "2024" license = "Apache-2.0" name = "warpgate-web" -version = "0.25.1" +version = "0.25.2" [dependencies] serde.workspace = true diff --git a/warpgate/Cargo.toml b/warpgate/Cargo.toml index f516d5a70..db1f3e889 100644 --- a/warpgate/Cargo.toml +++ b/warpgate/Cargo.toml @@ -2,7 +2,7 @@ edition = "2024" license = "Apache-2.0" name = "warpgate" -version = "0.25.1" +version = "0.25.2" publish = false [dependencies] From 95a646827b22b67164a34f2578708029e84ab34c Mon Sep 17 00:00:00 2001 From: Eugene Date: Tue, 9 Jun 2026 23:50:37 +0200 Subject: [PATCH 171/556] Update Cargo.lock --- Cargo.lock | 36 ++++++++++++++++++------------------ 1 file changed, 18 insertions(+), 18 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index 30a6830c3..f94b6da04 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -7593,7 +7593,7 @@ dependencies = [ [[package]] name = "warpgate" -version = "0.25.1" +version = "0.25.2" dependencies = [ "anyhow", "async-trait", @@ -7636,7 +7636,7 @@ dependencies = [ [[package]] name = "warpgate-admin" -version = "0.25.1" +version = "0.25.2" dependencies = [ "anyhow", "async-trait", @@ -7696,7 +7696,7 @@ dependencies = [ [[package]] name = "warpgate-ca" -version = "0.25.1" +version = "0.25.2" dependencies = [ "aws-lc-rs", "bytes", @@ -7717,7 +7717,7 @@ dependencies = [ [[package]] name = "warpgate-common" -version = "0.25.1" +version = "0.25.2" dependencies = [ "anyhow", "argon2 0.5.3", @@ -7770,7 +7770,7 @@ dependencies = [ [[package]] name = "warpgate-common-http" -version = "0.25.1" +version = "0.25.2" dependencies = [ "poem", "poem-openapi", @@ -7785,7 +7785,7 @@ dependencies = [ [[package]] name = "warpgate-core" -version = "0.25.1" +version = "0.25.2" dependencies = [ "anyhow", "argon2 0.5.3", @@ -7830,7 +7830,7 @@ dependencies = [ [[package]] name = "warpgate-database-protocols" -version = "0.25.1" +version = "0.25.2" dependencies = [ "bitflags 2.13.0", "bytes", @@ -7843,7 +7843,7 @@ dependencies = [ [[package]] name = "warpgate-db-entities" -version = "0.25.1" +version = "0.25.2" dependencies = [ "bytes", "ipnet", @@ -7862,7 +7862,7 @@ dependencies = [ [[package]] name = "warpgate-db-migrations" -version = "0.25.1" +version = "0.25.2" dependencies = [ "data-encoding", "regex", @@ -7880,7 +7880,7 @@ dependencies = [ [[package]] name = "warpgate-ldap" -version = "0.25.1" +version = "0.25.2" dependencies = [ "anyhow", "ldap3", @@ -7896,7 +7896,7 @@ dependencies = [ [[package]] name = "warpgate-protocol-http" -version = "0.25.1" +version = "0.25.2" dependencies = [ "anyhow", "async-trait", @@ -7935,7 +7935,7 @@ dependencies = [ [[package]] name = "warpgate-protocol-kubernetes" -version = "0.25.1" +version = "0.25.2" dependencies = [ "anyhow", "async-trait", @@ -7974,7 +7974,7 @@ dependencies = [ [[package]] name = "warpgate-protocol-mysql" -version = "0.25.1" +version = "0.25.2" dependencies = [ "anyhow", "async-trait", @@ -8002,7 +8002,7 @@ dependencies = [ [[package]] name = "warpgate-protocol-postgres" -version = "0.25.1" +version = "0.25.2" dependencies = [ "anyhow", "async-trait", @@ -8029,7 +8029,7 @@ dependencies = [ [[package]] name = "warpgate-protocol-ssh" -version = "0.25.1" +version = "0.25.2" dependencies = [ "anyhow", "async-trait", @@ -8062,7 +8062,7 @@ dependencies = [ [[package]] name = "warpgate-sso" -version = "0.25.1" +version = "0.25.2" dependencies = [ "bytes", "data-encoding", @@ -8082,7 +8082,7 @@ dependencies = [ [[package]] name = "warpgate-tls" -version = "0.25.1" +version = "0.25.2" dependencies = [ "poem", "poem-openapi", @@ -8102,7 +8102,7 @@ dependencies = [ [[package]] name = "warpgate-web" -version = "0.25.1" +version = "0.25.2" dependencies = [ "rust-embed", "serde", From 685c6bb4fc79307d391d8800f4b8335035943f91 Mon Sep 17 00:00:00 2001 From: Eugene Date: Wed, 10 Jun 2026 00:24:04 +0200 Subject: [PATCH 172/556] deny default dependencies --- Cargo.lock | 3 - Cargo.toml | 100 ++++++++++++++++++------ warpgate-admin/Cargo.toml | 16 ++-- warpgate-aws/Cargo.toml | 14 ++-- warpgate-ca/Cargo.toml | 29 ++++--- warpgate-common-http/Cargo.toml | 10 +-- warpgate-common/Cargo.toml | 32 ++++---- warpgate-core/Cargo.toml | 24 +++--- warpgate-database-protocols/Cargo.toml | 6 +- warpgate-db-entities/Cargo.toml | 18 ++--- warpgate-db-migrations/Cargo.toml | 17 ++-- warpgate-ldap/Cargo.toml | 6 +- warpgate-protocol-http/Cargo.toml | 35 ++++----- warpgate-protocol-kubernetes/Cargo.toml | 19 +++-- warpgate-protocol-mysql/Cargo.toml | 34 ++++---- warpgate-protocol-postgres/Cargo.toml | 38 ++++----- warpgate-protocol-ssh/Cargo.toml | 25 +++--- warpgate-sso/Cargo.toml | 32 +++++--- warpgate-tls/Cargo.toml | 12 +-- warpgate-web-ssh/Cargo.toml | 6 +- warpgate-web/Cargo.toml | 6 +- warpgate/Cargo.toml | 37 +++++---- 22 files changed, 293 insertions(+), 226 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index f94b6da04..aeaa0ae96 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -752,7 +752,6 @@ dependencies = [ "base64-simd", "bytes", "bytes-utils", - "futures-core", "http 0.2.12", "http 1.4.1", "http-body 0.4.6", @@ -765,8 +764,6 @@ dependencies = [ "ryu", "serde", "time", - "tokio", - "tokio-util", ] [[package]] diff --git a/Cargo.toml b/Cargo.toml index c17165174..67a93ed75 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -27,20 +27,40 @@ resolver = "2" [workspace.dependencies] anyhow = { version = "1.0", default-features = false, features = ["std"] } -argon2 = { version = "0.5", default-features = false, features = ["password-hash", "alloc"] } +async-trait = { version = "0.1", default-features = false } +argon2 = { version = "0.5", default-features = false, features = [ + "password-hash", + "alloc", +] } bytes = { version = "1.4", default-features = false } -data-encoding = { version = "2.3", default-features = false, features = ["alloc", "std"] } -ipnet = "2" +data-encoding = { version = "2.3", default-features = false, features = [ + "alloc", + "std", +] } +ipnet = { version = "2", default-features = false } serde = { version = "1.0", features = ["derive"], default-features = false } serde_json = { version = "1.0", default-features = false } -russh = { version = "0.61.1", features = ["des", "rsa", "aws-lc-rs"], default-features = false } +russh = { version = "0.61.1", features = [ + "des", + "rsa", + "aws-lc-rs", +], default-features = false } futures = { version = "0.3", default-features = false } -tokio-stream = { version = "0.1.17", features = ["net"], default-features = false } +http = { version = "1.4", default-features = false } +tokio-stream = { version = "0.1.17", features = [ + "net", +], default-features = false } tokio-rustls = { version = "0.26", default-features = false } enum_dispatch = { version = "0.3.13", default-features = false } rustls = { version = "0.23", default-features = false, features = ["tls12"] } -sqlx = { version = "0.8", features = ["tls-rustls-aws-lc-rs"], default-features = false } -sea-orm = { version = "1.0", default-features = false, features = ["runtime-tokio", "macros", "with-time"] } +sqlx = { version = "0.8", features = [ + "tls-rustls-aws-lc-rs", +], default-features = false } +sea-orm = { version = "1.0", default-features = false, features = [ + "runtime-tokio", + "macros", + "with-time", +] } sea-orm-migration = { version = "1.0", default-features = false, features = [ "cli", ] } @@ -61,24 +81,57 @@ poem-openapi = { version = "5.1", features = [ "cookie", "time", ], default-features = false } -password-hash = { version = "0.5", features = ["std"], default-features = false } +password-hash = { version = "0.5", features = [ + "std", +], default-features = false } delegate = { version = "0.13", default-features = false } +subtle = { version = "2", default-features = false } tracing = { version = "0.1", default-features = false } -schemars = { version = "0.9.0", default-features = false, features = ["derive", "std"] } -ldap3 = { version = "0.12", default-features = false, features = ["tls-rustls-aws-lc-rs"] } -rustls-pki-types = { version = "1.13", default-features = false, features = ["alloc", "std"] } +schemars = { version = "0.9.0", default-features = false, features = [ + "derive", + "std", +] } +ldap3 = { version = "0.12", default-features = false, features = [ + "tls-rustls-aws-lc-rs", +] } +rustls-pki-types = { version = "1.13", default-features = false, features = [ + "alloc", + "std", +] } thiserror = { version = "2", default-features = false } rand = { version = "0.10", default-features = false } rand_chacha = { version = "0.10", default-features = false } -rand_core = { version = "0.10" } -dialoguer = { version = "0.12", default-features = false, features = ["editor", "password"] } -tokio = { version = "1.52", features = ["tracing", "signal", "macros", "rt-multi-thread", "io-util"], default-features = false } -governor = { version = "0.10.0", default-features = false, features = ["std", "quanta", "jitter"] } -rcgen = { version = "0.14", features = ["zeroize", "crypto", "aws_lc_rs", "pem", "x509-parser"], default-features = false } -x509-parser = "0.18.1" -uuid = { version = "1.23", features = ["v4", "serde"], default-features = false } +rand_core = { version = "0.10", default-features = false } +dialoguer = { version = "0.12", default-features = false, features = [ + "editor", + "password", +] } +tokio = { version = "1.52", features = [ + "tracing", + "signal", + "macros", + "rt-multi-thread", + "io-util", +], default-features = false } +governor = { version = "0.10.0", default-features = false, features = [ + "std", + "quanta", + "jitter", +] } +rcgen = { version = "0.14", features = [ + "zeroize", + "crypto", + "aws_lc_rs", + "pem", + "x509-parser", +], default-features = false } +x509-parser = { version = "0.18.1", default-features = false } +uuid = { version = "1.23", features = [ + "v4", + "serde", +], default-features = false } reqwest = { version = "0.13", features = [ - "http2", # required for connecting to targets behind AWS ELB + "http2", # required for connecting to targets behind AWS ELB "rustls-no-provider", "stream", "gzip", @@ -90,9 +143,12 @@ reqwest_12 = { package = "reqwest", version = "0.12", features = [ "gzip", ], default-features = false } # separate copy to control features on openidconnect->oauth2->reqwest regex = { version = "1.6", default-features = false, features = ["std"] } -tokio-tungstenite = { version = "0.27", features = ["rustls-tls-native-roots", "connect"], default-features = false } -reqwest-websocket = "0.6.0" -time = "0.3" +tokio-tungstenite = { version = "0.27", features = [ + "rustls-tls-native-roots", + "connect", +], default-features = false } +reqwest-websocket = { version = "0.6.0", default-features = false } +time = { version = "0.3", default-features = false } url = { version = "2.4", default-features = false } [profile.release] diff --git a/warpgate-admin/Cargo.toml b/warpgate-admin/Cargo.toml index 3cdf8fa21..63f715809 100644 --- a/warpgate-admin/Cargo.toml +++ b/warpgate-admin/Cargo.toml @@ -1,23 +1,25 @@ [package] -edition = "2024" -license = "Apache-2.0" name = "warpgate-admin" version = "0.25.2" +edition = "2024" +license = "Apache-2.0" publish = false [dependencies] anyhow.workspace = true -async-trait = { version = "0.1", default-features = false } +async-trait.workspace = true bytes.workspace = true futures.workspace = true hex.workspace = true +ipnet = { version = "2", default-features = false } mime_guess = { version = "2.0", default-features = false } poem.workspace = true poem-openapi.workspace = true rcgen.workspace = true -rustls-pki-types.workspace = true +regex.workspace = true russh.workspace = true rust-embed = { version = "8.3", default-features = false } +rustls-pki-types.workspace = true sea-orm.workspace = true serde.workspace = true serde_json.workspace = true @@ -29,11 +31,9 @@ uuid.workspace = true warpgate-ca = { path = "../warpgate-ca" } warpgate-common = { path = "../warpgate-common" } warpgate-common-http = { path = "../warpgate-common-http" } -warpgate-tls = { path = "../warpgate-tls" } warpgate-core = { path = "../warpgate-core" } warpgate-db-entities = { path = "../warpgate-db-entities" } warpgate-ldap = { path = "../warpgate-ldap" } -warpgate-protocol-ssh = { path = "../warpgate-protocol-ssh" } warpgate-protocol-kubernetes = { path = "../warpgate-protocol-kubernetes" } -ipnet = "2" -regex.workspace = true +warpgate-protocol-ssh = { path = "../warpgate-protocol-ssh" } +warpgate-tls = { path = "../warpgate-tls" } diff --git a/warpgate-aws/Cargo.toml b/warpgate-aws/Cargo.toml index 45d712034..d5894041f 100644 --- a/warpgate-aws/Cargo.toml +++ b/warpgate-aws/Cargo.toml @@ -1,8 +1,8 @@ [package] -edition = "2024" -license = "Apache-2.0" name = "warpgate-aws" version = "0.22.0-beta.5" +edition = "2024" +license = "Apache-2.0" publish = false [dependencies] @@ -11,6 +11,7 @@ aws-config = { version = "1", default-features = false, features = [ "rustls", "rt-tokio", ] } +aws-credential-types = { version = "1", default-features = false } aws-sdk-ec2 = { version = "1", default-features = false, features = [ "behavior-version-latest", "rustls", @@ -23,19 +24,18 @@ aws-sdk-eks = { version = "1", default-features = false, features = [ "behavior-version-latest", "rustls", ] } +aws-sdk-rds = { version = "1.130.0", default-features = false } aws-sdk-sts = { version = "1", default-features = false, features = [ "behavior-version-latest", "rustls", ] } aws-sigv4 = { version = "1", default-features = false } -aws-credential-types = { version = "1", default-features = false } aws-smithy-runtime-api = { version = "1", default-features = false } dashmap = { version = "6.2", default-features = false } -http = "1" +data-encoding.workspace = true +http.workspace = true reqwest.workspace = true +thiserror.workspace = true tokio.workspace = true tracing.workspace = true url.workspace = true -data-encoding.workspace = true -aws-sdk-rds = "1.130.0" -thiserror.workspace = true diff --git a/warpgate-ca/Cargo.toml b/warpgate-ca/Cargo.toml index 492d4e6ed..ca74a63f0 100644 --- a/warpgate-ca/Cargo.toml +++ b/warpgate-ca/Cargo.toml @@ -1,24 +1,27 @@ [package] -edition = "2024" -license = "Apache-2.0" name = "warpgate-ca" version = "0.25.2" +edition = "2024" +license = "Apache-2.0" publish = false [dependencies] +aws-lc-rs = { version = "1", default-features = false } bytes.workspace = true +const-oid = { version = "0.9", default-features = false } +data-encoding.workspace = true +der = { version = "0.7", default-features = false } + +hex.workspace = true +pem = { version = "3.0", default-features = false } +rcgen.workspace = true +spki = { version = "0.7", default-features = false } thiserror.workspace = true tokio.workspace = true -data-encoding.workspace = true tracing.workspace = true -rcgen.workspace = true -x509-parser.workspace = true -x509-cert = { version = "0.2", features = ["builder", "signature"] } -aws-lc-rs = "1" -der = "0.7" -pem = "3.0" -spki = "0.7" -const-oid = "0.9" uuid.workspace = true - -hex.workspace = true +x509-cert = { version = "0.2", features = [ + "builder", + "signature", +], default-features = false } +x509-parser.workspace = true diff --git a/warpgate-common-http/Cargo.toml b/warpgate-common-http/Cargo.toml index dd27a721a..e24981343 100644 --- a/warpgate-common-http/Cargo.toml +++ b/warpgate-common-http/Cargo.toml @@ -1,17 +1,17 @@ [package] -edition = "2024" -license = "Apache-2.0" name = "warpgate-common-http" version = "0.25.2" +edition = "2024" +license = "Apache-2.0" publish = false [dependencies] -warpgate-common = { path = "../warpgate-common" } -warpgate-core = { path = "../warpgate-core" } poem.workspace = true poem-openapi.workspace = true serde.workspace = true tokio.workspace = true tracing.workspace = true -uuid.workspace = true url.workspace = true +uuid.workspace = true +warpgate-common = { path = "../warpgate-common" } +warpgate-core = { path = "../warpgate-core" } diff --git a/warpgate-common/Cargo.toml b/warpgate-common/Cargo.toml index f0ae36998..e3f3c0b66 100644 --- a/warpgate-common/Cargo.toml +++ b/warpgate-common/Cargo.toml @@ -1,8 +1,8 @@ [package] -edition = "2024" -license = "Apache-2.0" name = "warpgate-common" version = "0.25.2" +edition = "2024" +license = "Apache-2.0" publish = false [[bin]] @@ -11,10 +11,13 @@ path = "src/config_schema.rs" [dependencies] anyhow.workspace = true -clap = { version = "4.0", features = ["derive", "std"], default-features = false } argon2.workspace = true -async-trait = { version = "0.1", default-features = false } +async-trait.workspace = true bytes.workspace = true +clap = { version = "4.0", features = [ + "derive", + "std", +], default-features = false } data-encoding.workspace = true delegate.workspace = true futures.workspace = true @@ -25,36 +28,35 @@ ipnet.workspace = true password-hash.workspace = true poem.workspace = true poem-openapi.workspace = true +rand.workspace = true rand_chacha.workspace = true rand_core.workspace = true -rand.workspace = true rcgen.workspace = true -reqwest.workspace = true -reqwest.features = ["json"] +reqwest = { workspace = true, features = ["json"] } reqwest-websocket.workspace = true russh.workspace = true +rustls.workspace = true rustls-native-certs = { version = "0.8", default-features = false } rustls-pki-types.workspace = true -rustls.workspace = true schemars.workspace = true sea-orm.workspace = true serde.workspace = true serde_json.workspace = true thiserror.workspace = true time.workspace = true +tokio.workspace = true tokio-rustls.workspace = true tokio-stream.workspace = true tokio-tungstenite.workspace = true -tokio.workspace = true totp-rs = { version = "5.0", features = ["otpauth"], default-features = false } -tracing-core = { version = "0.1", default-features = false } tracing.workspace = true +tracing-core = { version = "0.1", default-features = false } url.workspace = true uuid.workspace = true -warpgate-aws = { path = "../warpgate-aws", default-features = false } -warpgate-ca = { path = "../warpgate-ca", default-features = false } +warpgate-aws = { path = "../warpgate-aws" } +warpgate-ca = { path = "../warpgate-ca" } warpgate-ldap = { path = "../warpgate-ldap" } -warpgate-sso = { path = "../warpgate-sso", default-features = false } -warpgate-tls = { path = "../warpgate-tls", default-features = false } +warpgate-sso = { path = "../warpgate-sso" } +warpgate-tls = { path = "../warpgate-tls" } webpki = { version = "0.22", default-features = false } -x509-parser = "0.18.1" +x509-parser = { version = "0.18.1", default-features = false } diff --git a/warpgate-core/Cargo.toml b/warpgate-core/Cargo.toml index a15263b3d..750b5fc4e 100644 --- a/warpgate-core/Cargo.toml +++ b/warpgate-core/Cargo.toml @@ -1,28 +1,25 @@ [package] -edition = "2024" -license = "Apache-2.0" name = "warpgate-core" version = "0.25.2" +edition = "2024" +license = "Apache-2.0" publish = false [dependencies] -warpgate-common = { path = "../warpgate-common" } -warpgate-db-entities = { path = "../warpgate-db-entities" } -warpgate-db-migrations = { path = "../warpgate-db-migrations" } -warpgate-ldap = { path = "../warpgate-ldap" } anyhow.workspace = true argon2.workspace = true -async-trait = "0.1" +async-trait.workspace = true bytes.workspace = true data-encoding.workspace = true dialoguer.workspace = true enum_dispatch.workspace = true -humantime-serde = "1.1" futures.workspace = true +governor.workspace = true +humantime-serde = { version = "1.1", default-features = false } ipnet.workspace = true ldap3.workspace = true -packet = "0.1" +packet = { version = "0.1", default-features = false } password-hash.workspace = true poem.workspace = true poem-openapi.workspace = true @@ -30,11 +27,12 @@ rand.workspace = true rand_chacha.workspace = true rand_core.workspace = true russh.workspace = true +rustls.workspace = true sea-orm.workspace = true serde.workspace = true serde_json.workspace = true -time.workspace = true thiserror.workspace = true +time.workspace = true tokio.workspace = true totp-rs = { version = "5.0", features = ["otpauth"], default-features = false } tracing.workspace = true @@ -42,10 +40,12 @@ tracing-core = { version = "0.1", default-features = false } tracing-subscriber = { version = "0.3", default-features = false } url.workspace = true uuid.workspace = true +warpgate-common = { path = "../warpgate-common" } +warpgate-db-entities = { path = "../warpgate-db-entities" } +warpgate-db-migrations = { path = "../warpgate-db-migrations" } +warpgate-ldap = { path = "../warpgate-ldap" } warpgate-sso = { path = "../warpgate-sso", default-features = false } -rustls.workspace = true webpki = { version = "0.22", default-features = false } -governor.workspace = true [features] postgres = ["sea-orm/sqlx-postgres"] diff --git a/warpgate-database-protocols/Cargo.toml b/warpgate-database-protocols/Cargo.toml index 1bec7e0aa..f7cd6943e 100644 --- a/warpgate-database-protocols/Cargo.toml +++ b/warpgate-database-protocols/Cargo.toml @@ -2,18 +2,17 @@ name = "warpgate-database-protocols" version = "0.25.2" description = "Core of SQLx, the rust SQL toolkit. Just the database protocol parts." -license = "MIT OR Apache-2.0" -edition = "2024" authors = [ "Ryan Leckey ", "Austin Bonander ", "Chloe Ross ", "Daniel Akhterov ", ] +edition = "2024" +license = "MIT OR Apache-2.0" publish = false [dependencies] -tokio.workspace = true bitflags = { version = "2", default-features = false } bytes.workspace = true futures-core = { version = "0.3", default-features = false } @@ -23,3 +22,4 @@ futures-util = { version = "0.3", default-features = false, features = [ ] } memchr = { version = "2.8", default-features = false } thiserror.workspace = true +tokio.workspace = true diff --git a/warpgate-db-entities/Cargo.toml b/warpgate-db-entities/Cargo.toml index 67865ad51..5ad8f1fc8 100644 --- a/warpgate-db-entities/Cargo.toml +++ b/warpgate-db-entities/Cargo.toml @@ -1,25 +1,21 @@ [package] -edition = "2024" -license = "Apache-2.0" name = "warpgate-db-entities" version = "0.25.2" +edition = "2024" +license = "Apache-2.0" publish = false [dependencies] bytes = { version = "1.4", default-features = false } -poem-openapi.workspace = true -sqlx.workspace = true ipnet.workspace = true -sea-orm = { workspace = true, features = [ - "macros", - "with-uuid", - "with-json", -], default-features = false } +poem-openapi.workspace = true +sea-orm = { workspace = true, features = ["macros", "with-uuid", "with-json"] } +secrecy = { version = "0.10", default-features = false } serde.workspace = true serde_json.workspace = true +sqlx.workspace = true time.workspace = true uuid.workspace = true warpgate-common = { path = "../warpgate-common", default-features = false } -secrecy = "0.10" +warpgate-ldap = { path = "../warpgate-ldap", default-features = false } warpgate-tls = { path = "../warpgate-tls", default-features = false } -warpgate-ldap = { path = "../warpgate-ldap" } diff --git a/warpgate-db-migrations/Cargo.toml b/warpgate-db-migrations/Cargo.toml index dc54b6f99..00ac1b7f2 100644 --- a/warpgate-db-migrations/Cargo.toml +++ b/warpgate-db-migrations/Cargo.toml @@ -1,27 +1,24 @@ [package] -edition = "2024" -license = "Apache-2.0" name = "warpgate-db-migrations" version = "0.25.2" +edition = "2024" +license = "Apache-2.0" publish = false [lib] [dependencies] -tokio.workspace = true data-encoding.workspace = true regex.workspace = true -sea-orm = { workspace = true, features = [ - "with-uuid", - "with-json", -], default-features = false } -sea-orm-migration.workspace = true russh.workspace = true +sea-orm = { workspace = true, features = ["with-uuid", "with-json"] } +sea-orm-migration.workspace = true +serde.workspace = true +serde_json.workspace = true time.workspace = true +tokio.workspace = true tracing.workspace = true uuid.workspace = true -serde_json.workspace = true -serde.workspace = true warpgate-ca = { path = "../warpgate-ca", default-features = false } [features] diff --git a/warpgate-ldap/Cargo.toml b/warpgate-ldap/Cargo.toml index 2b9fe47cb..7fa65ba11 100644 --- a/warpgate-ldap/Cargo.toml +++ b/warpgate-ldap/Cargo.toml @@ -1,17 +1,17 @@ [package] -edition = "2024" -license = "Apache-2.0" name = "warpgate-ldap" version = "0.25.2" +edition = "2024" +license = "Apache-2.0" publish = false [dependencies] anyhow.workspace = true ldap3.workspace = true +poem-openapi.workspace = true serde.workspace = true serde_json.workspace = true thiserror.workspace = true -poem-openapi.workspace = true tokio.workspace = true tracing.workspace = true uuid.workspace = true diff --git a/warpgate-protocol-http/Cargo.toml b/warpgate-protocol-http/Cargo.toml index bd7c44535..abf41a65c 100644 --- a/warpgate-protocol-http/Cargo.toml +++ b/warpgate-protocol-http/Cargo.toml @@ -1,42 +1,41 @@ [package] -edition = "2024" -license = "Apache-2.0" name = "warpgate-protocol-http" version = "0.25.2" +edition = "2024" +license = "Apache-2.0" publish = false [dependencies] -anyhow = "1.0" -async-trait = "0.1" -cookie = "0.18" +anyhow.workspace = true +async-trait.workspace = true +cookie = { version = "0.18", default-features = false } data-encoding.workspace = true delegate.workspace = true futures.workspace = true -http = { version = "1.4", default-features = false } +http.workspace = true +percent-encoding = { version = "2.1", default-features = false } poem.workspace = true poem-openapi.workspace = true -reqwest.workspace = true -reqwest.features = ["json"] +regex.workspace = true +reqwest = { workspace = true, features = ["json"] } sea-orm.workspace = true serde.workspace = true serde_json.workspace = true +subtle.workspace = true time.workspace = true tokio.workspace = true tokio-tungstenite.workspace = true tracing.workspace = true +url.workspace = true +uuid.workspace = true warpgate-admin = { path = "../warpgate-admin", default-features = false } -warpgate-web-ssh = { path = "../warpgate-web-ssh", default-features = false } warpgate-aws = { path = "../warpgate-aws", default-features = false } -warpgate-common = { path = "../warpgate-common", default-features = false } -warpgate-common-http = { path = "../warpgate-common-http" } warpgate-ca = { path = "../warpgate-ca", default-features = false } -warpgate-tls = { path = "../warpgate-tls", default-features = false } +warpgate-common = { path = "../warpgate-common", default-features = false } +warpgate-common-http = { path = "../warpgate-common-http", default-features = false } warpgate-core = { path = "../warpgate-core", default-features = false } warpgate-db-entities = { path = "../warpgate-db-entities", default-features = false } -warpgate-web = { path = "../warpgate-web", default-features = false } warpgate-sso = { path = "../warpgate-sso", default-features = false } -percent-encoding = { version = "2.1", default-features = false } -subtle = "2" -uuid.workspace = true -regex.workspace = true -url.workspace = true +warpgate-tls = { path = "../warpgate-tls", default-features = false } +warpgate-web = { path = "../warpgate-web", default-features = false } +warpgate-web-ssh = { path = "../warpgate-web-ssh", default-features = false } diff --git a/warpgate-protocol-kubernetes/Cargo.toml b/warpgate-protocol-kubernetes/Cargo.toml index 838a8068b..aec1748b2 100644 --- a/warpgate-protocol-kubernetes/Cargo.toml +++ b/warpgate-protocol-kubernetes/Cargo.toml @@ -1,42 +1,41 @@ [package] -edition = "2024" -license = "Apache-2.0" name = "warpgate-protocol-kubernetes" version = "0.25.2" +edition = "2024" +license = "Apache-2.0" publish = false [dependencies] anyhow.workspace = true -async-trait = { version = "0.1", default-features = false } +async-trait.workspace = true base64 = { version = "0.22", default-features = false } bytes.workspace = true dashmap = { version = "6.2", default-features = false } futures.workspace = true -http = { version = "1.4", default-features = false } +http.workspace = true md5 = { version = "0.8", default-features = false } poem.workspace = true poem-openapi.workspace = true regex.workspace = true -reqwest.workspace = true -reqwest.features = ["json"] +reqwest = { workspace = true, features = ["json"] } +reqwest-websocket.workspace = true rustls.workspace = true sea-orm.workspace = true secrecy = { version = "0.10", default-features = false } serde = { version = "1.0", features = ["derive"], default-features = false } serde_json.workspace = true thiserror.workspace = true +time.workspace = true tokio.workspace = true tokio-rustls = { version = "0.26", default-features = false } -time.workspace = true +tokio-tungstenite.workspace = true tracing.workspace = true url.workspace = true uuid.workspace = true warpgate-aws = { path = "../warpgate-aws", default-features = false } +warpgate-ca = { path = "../warpgate-ca", default-features = false } warpgate-common = { path = "../warpgate-common", default-features = false } warpgate-common-http = { path = "../warpgate-common-http", default-features = false } warpgate-core = { path = "../warpgate-core", default-features = false } warpgate-db-entities = { path = "../warpgate-db-entities", default-features = false } warpgate-tls = { path = "../warpgate-tls", default-features = false } -warpgate-ca = { path = "../warpgate-ca", default-features = false } -tokio-tungstenite.workspace = true -reqwest-websocket.workspace = true diff --git a/warpgate-protocol-mysql/Cargo.toml b/warpgate-protocol-mysql/Cargo.toml index 13a545e3d..69a44687a 100644 --- a/warpgate-protocol-mysql/Cargo.toml +++ b/warpgate-protocol-mysql/Cargo.toml @@ -1,30 +1,30 @@ [package] -edition = "2024" -license = "Apache-2.0" name = "warpgate-protocol-mysql" version = "0.25.2" +edition = "2024" +license = "Apache-2.0" publish = false [dependencies] -warpgate-aws = { path = "../warpgate-aws", default-features = false } -warpgate-common = { path = "../warpgate-common", default-features = false } -warpgate-tls = { path = "../warpgate-tls", default-features = false } -warpgate-core = { path = "../warpgate-core", default-features = false } -warpgate-db-entities = { path = "../warpgate-db-entities", default-features = false } -warpgate-database-protocols = { path = "../warpgate-database-protocols", default-features = false } anyhow.workspace = true -async-trait = { version = "0.1", default-features = false } -futures.workspace = true -tokio.workspace = true -tracing.workspace = true -uuid.workspace = true +async-trait.workspace = true bytes.workspace = true -mysql_common = { version = "0.34", default-features = false } flate2 = { version = "1", features = ["zlib"], default-features = false } -rand.workspace = true -sha1 = { version = "0.10", default-features = false } +futures.workspace = true +mysql_common = { version = "0.34", default-features = false } password-hash.workspace = true +rand.workspace = true rustls.workspace = true -tokio-rustls.workspace = true +sha1 = { version = "0.10", default-features = false } thiserror.workspace = true +tokio.workspace = true +tokio-rustls.workspace = true +tracing.workspace = true +uuid.workspace = true +warpgate-aws = { path = "../warpgate-aws", default-features = false } +warpgate-common = { path = "../warpgate-common", default-features = false } +warpgate-core = { path = "../warpgate-core", default-features = false } +warpgate-database-protocols = { path = "../warpgate-database-protocols", default-features = false } +warpgate-db-entities = { path = "../warpgate-db-entities", default-features = false } +warpgate-tls = { path = "../warpgate-tls", default-features = false } webpki = { version = "0.22", default-features = false } diff --git a/warpgate-protocol-postgres/Cargo.toml b/warpgate-protocol-postgres/Cargo.toml index 31c71a8c5..baec2d657 100644 --- a/warpgate-protocol-postgres/Cargo.toml +++ b/warpgate-protocol-postgres/Cargo.toml @@ -1,30 +1,20 @@ [package] -edition = "2024" -license = "Apache-2.0" name = "warpgate-protocol-postgres" version = "0.25.2" +edition = "2024" +license = "Apache-2.0" publish = false [dependencies] -warpgate-aws = { path = "../warpgate-aws", default-features = false } -warpgate-common = { path = "../warpgate-common", default-features = false } -warpgate-common-http = { path = "../warpgate-common-http", default-features = false } -warpgate-tls = { path = "../warpgate-tls", default-features = false } -warpgate-core = { path = "../warpgate-core", default-features = false } anyhow.workspace = true -async-trait = { version = "0.1", default-features = false } -tokio.workspace = true -rand.workspace = true -tracing.workspace = true -uuid.workspace = true +async-trait.workspace = true bytes.workspace = true -rustls.workspace = true -tokio-rustls.workspace = true -thiserror.workspace = true -rustls-native-certs = { version = "0.8", default-features = false } +futures.workspace = true +humantime = { version = "2.1", default-features = false } pgwire = { version = "0.40", default-features = false, features = [ "server-api-aws-lc-rs", ] } +rand.workspace = true rsasl = { version = "2.3", default-features = false, features = [ "config_builder", "scram-sha-2", @@ -32,6 +22,16 @@ rsasl = { version = "2.3", default-features = false, features = [ "plain", "provider", ] } -futures.workspace = true -humantime = { version = "2.1", default-features = false } -socket2 = { version = "0.6", features = ["all"] } +rustls.workspace = true +rustls-native-certs = { version = "0.8", default-features = false } +socket2 = { version = "0.6", features = ["all"], default-features = false } +thiserror.workspace = true +tokio.workspace = true +tokio-rustls.workspace = true +tracing.workspace = true +uuid.workspace = true +warpgate-aws = { path = "../warpgate-aws", default-features = false } +warpgate-common = { path = "../warpgate-common", default-features = false } +warpgate-common-http = { path = "../warpgate-common-http", default-features = false } +warpgate-core = { path = "../warpgate-core", default-features = false } +warpgate-tls = { path = "../warpgate-tls", default-features = false } diff --git a/warpgate-protocol-ssh/Cargo.toml b/warpgate-protocol-ssh/Cargo.toml index 266ebf60a..9b7aa1f30 100644 --- a/warpgate-protocol-ssh/Cargo.toml +++ b/warpgate-protocol-ssh/Cargo.toml @@ -1,32 +1,37 @@ [package] -edition = "2024" -license = "Apache-2.0" name = "warpgate-protocol-ssh" version = "0.25.2" +edition = "2024" +license = "Apache-2.0" publish = false [dependencies] -termcolor = { version = "1", default-features = false } anyhow.workspace = true -async-trait = { version = "0.1", default-features = false } +async-trait.workspace = true bimap = { version = "0.6", default-features = false, features = ["std"] } bytes.workspace = true -dialoguer.workspace = true curve25519-dalek = { version = "4.0.0", default-features = false } # pin due to build fail on x86 +dialoguer.workspace = true ed25519-dalek = { version = "2.0.0", default-features = false } # pin due to build fail on x86 in 2.1 futures.workspace = true -ratatui = { version = "0.29", default-features = false, features = ["crossterm", "unstable-backend-writer"] } -termwiz = { version = "0.22", default-features = false } -tui-input = "0.14" +ratatui = { version = "0.29", default-features = false, features = [ + "crossterm", + "unstable-backend-writer", +] } russh.workspace = true -serde.workspace = true sea-orm.workspace = true +serde.workspace = true +termcolor = { version = "1", default-features = false } +termwiz = { version = "0.22", default-features = false } thiserror.workspace = true time = { version = "0.3", default-features = false } tokio.workspace = true tracing.workspace = true -uuid.workspace = true +tui-input = { version = "0.14", default-features = false, features = [ + "ratatui-crossterm", +] } url.workspace = true +uuid.workspace = true warpgate-aws = { path = "../warpgate-aws", default-features = false } warpgate-common = { path = "../warpgate-common", default-features = false } warpgate-common-http = { path = "../warpgate-common-http", default-features = false } diff --git a/warpgate-sso/Cargo.toml b/warpgate-sso/Cargo.toml index 75de58ebf..7fecec849 100644 --- a/warpgate-sso/Cargo.toml +++ b/warpgate-sso/Cargo.toml @@ -1,26 +1,32 @@ [package] -edition = "2024" -license = "Apache-2.0" name = "warpgate-sso" version = "0.25.2" +edition = "2024" +license = "Apache-2.0" publish = false [dependencies] bytes.workspace = true -thiserror.workspace = true -tokio.workspace = true -tracing.workspace = true +data-encoding.workspace = true +futures.workspace = true +jsonwebtoken = { version = "10", default-features = false, features = [ + "use_pem", + "aws_lc_rs", +] } openidconnect = { version = "4.0", default-features = false, features = [ "reqwest", "accept-string-booleans", ] } -yup-oauth2 = "12" -reqwest_12.workspace = true -reqwest_12.features = ["json"] +reqwest_12 = { workspace = true, features = ["json"] } +schemars.workspace = true serde.workspace = true serde_json.workspace = true -jsonwebtoken = { version = "10", default-features = false, features = ["use_pem", "aws_lc_rs"] } -data-encoding.workspace = true -futures.workspace = true -schemars.workspace = true -subtle = "2" +subtle.workspace = true +thiserror.workspace = true +tokio.workspace = true +tracing.workspace = true +yup-oauth2 = { version = "12", default-features = false, features = [ + "service-account", + "hyper-rustls", + "ring", +] } diff --git a/warpgate-tls/Cargo.toml b/warpgate-tls/Cargo.toml index 4413f4516..8242aebf2 100644 --- a/warpgate-tls/Cargo.toml +++ b/warpgate-tls/Cargo.toml @@ -1,22 +1,22 @@ [package] -edition = "2024" -license = "Apache-2.0" name = "warpgate-tls" version = "0.25.2" +edition = "2024" +license = "Apache-2.0" publish = false [dependencies] poem.workspace = true poem-openapi.workspace = true +rustls.workspace = true rustls-native-certs = { version = "0.8", default-features = false } rustls-pki-types.workspace = true -rustls.workspace = true sea-orm.workspace = true -serde_json.workspace = true serde.workspace = true +serde_json.workspace = true thiserror.workspace = true -tokio-rustls.workspace = true tokio.workspace = true +tokio-rustls.workspace = true tracing.workspace = true webpki = { version = "0.22", default-features = false } -x509-parser = "0.18.1" +x509-parser = { version = "0.18.1", default-features = false } diff --git a/warpgate-web-ssh/Cargo.toml b/warpgate-web-ssh/Cargo.toml index 92fd57187..e3d233865 100644 --- a/warpgate-web-ssh/Cargo.toml +++ b/warpgate-web-ssh/Cargo.toml @@ -1,8 +1,8 @@ [package] -edition = "2024" -license = "Apache-2.0" name = "warpgate-web-ssh" version = "0.23.4" +edition = "2024" +license = "Apache-2.0" publish = false [dependencies] @@ -23,5 +23,5 @@ uuid.workspace = true warpgate-common = { path = "../warpgate-common" } warpgate-common-http = { path = "../warpgate-common-http" } warpgate-core = { path = "../warpgate-core" } -warpgate-db-entities = { path = "../warpgate-db-entities", default-features = false } +warpgate-db-entities = { path = "../warpgate-db-entities" } warpgate-protocol-ssh = { path = "../warpgate-protocol-ssh" } diff --git a/warpgate-web/Cargo.toml b/warpgate-web/Cargo.toml index 91d167722..68ec5f95c 100644 --- a/warpgate-web/Cargo.toml +++ b/warpgate-web/Cargo.toml @@ -1,11 +1,11 @@ [package] -edition = "2024" -license = "Apache-2.0" name = "warpgate-web" version = "0.25.2" +edition = "2024" +license = "Apache-2.0" [dependencies] -serde.workspace = true rust-embed = { version = "8.3", default-features = false } +serde.workspace = true serde_json.workspace = true thiserror.workspace = true diff --git a/warpgate/Cargo.toml b/warpgate/Cargo.toml index db1f3e889..d0d412788 100644 --- a/warpgate/Cargo.toml +++ b/warpgate/Cargo.toml @@ -1,15 +1,18 @@ [package] -edition = "2024" -license = "Apache-2.0" name = "warpgate" version = "0.25.2" +edition = "2024" +license = "Apache-2.0" publish = false [dependencies] anyhow.workspace = true -async-trait = { version = "0.1", default-features = false } +async-trait.workspace = true bytes.workspace = true -clap = { version = "4.0", features = ["derive", "env"], default-features = false } +clap = { version = "4.0", features = [ + "derive", + "env", +], default-features = false } config = { version = "0.15", features = ["yaml"], default-features = false } console = { version = "0.16", default-features = false } console-subscriber = { version = "0.5", optional = true, default-features = false } @@ -17,18 +20,23 @@ data-encoding.workspace = true dialoguer.workspace = true enum_dispatch.workspace = true futures.workspace = true -notify = { version = "8.0", default-features = false, features = ["fsevent-sys"] } +notify = { version = "8.0", default-features = false, features = [ + "fsevent-sys", +] } rcgen.workspace = true -reqwest.workspace = true -reqwest.features = ["json"] +reqwest = { workspace = true, features = ["json"] } rustls.workspace = true +schemars.workspace = true +sea-orm.workspace = true serde_json.workspace = true serde_yaml = { version = "0.9", default-features = false } -sea-orm.workspace = true time = { version = "0.3", default-features = false } tokio.workspace = true tracing.workspace = true -tracing-log = { version = "0.2" } +tracing-log = { version = "0.2", default-features = false, features = [ + "log-tracer", + "std", +] } tracing-subscriber = { version = "0.3", features = [ "ansi", "env-filter", @@ -36,20 +44,16 @@ tracing-subscriber = { version = "0.3", features = [ ], default-features = false } uuid = { version = "1.23", default-features = false } warpgate-admin = { path = "../warpgate-admin" } -warpgate-common = { path = "../warpgate-common" } warpgate-ca = { path = "../warpgate-ca" } +warpgate-common = { path = "../warpgate-common" } warpgate-core = { path = "../warpgate-core" } warpgate-db-entities = { path = "../warpgate-db-entities" } warpgate-protocol-http = { path = "../warpgate-protocol-http" } +warpgate-protocol-kubernetes = { path = "../warpgate-protocol-kubernetes" } warpgate-protocol-mysql = { path = "../warpgate-protocol-mysql" } warpgate-protocol-postgres = { path = "../warpgate-protocol-postgres" } warpgate-protocol-ssh = { path = "../warpgate-protocol-ssh" } -warpgate-protocol-kubernetes = { path = "../warpgate-protocol-kubernetes" } warpgate-tls = { path = "../warpgate-tls" } -schemars.workspace = true - -[target.'cfg(target_os = "linux")'.dependencies] -sd-notify = { version = "0.5", default-features = false } [features] default = ["sqlite"] @@ -57,3 +61,6 @@ tokio-console = ["dep:console-subscriber", "tokio/tracing"] postgres = ["warpgate-core/postgres"] mysql = ["warpgate-core/mysql"] sqlite = ["warpgate-core/sqlite"] + +[target.'cfg(target_os = "linux")'.dependencies] +sd-notify = { version = "0.5", default-features = false } From 3656007124d0690b0d5ccacf3d9083917ffc673e Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Wed, 10 Jun 2026 21:44:36 +0200 Subject: [PATCH 173/556] Bump sha1 from 0.10.6 to 0.11.0 (#2034) Signed-off-by: dependabot[bot] --- Cargo.lock | 2 +- warpgate-protocol-mysql/Cargo.toml | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index aeaa0ae96..1d7e6c253 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -7982,7 +7982,7 @@ dependencies = [ "password-hash 0.5.0", "rand 0.10.1", "rustls 0.23.40", - "sha1 0.10.6", + "sha1 0.11.0", "thiserror 2.0.18", "tokio", "tokio-rustls 0.26.4", diff --git a/warpgate-protocol-mysql/Cargo.toml b/warpgate-protocol-mysql/Cargo.toml index 69a44687a..8dca74099 100644 --- a/warpgate-protocol-mysql/Cargo.toml +++ b/warpgate-protocol-mysql/Cargo.toml @@ -15,7 +15,7 @@ mysql_common = { version = "0.34", default-features = false } password-hash.workspace = true rand.workspace = true rustls.workspace = true -sha1 = { version = "0.10", default-features = false } +sha1 = { version = "0.11", default-features = false } thiserror.workspace = true tokio.workspace = true tokio-rustls.workspace = true From 6ec2299ba82bd4baf6a40686ea98555c91ed8305 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Wed, 10 Jun 2026 21:44:51 +0200 Subject: [PATCH 174/556] Bump SonarSource/sonarqube-scan-action from 8.1.0 to 8.2.0 (#2033) Signed-off-by: dependabot[bot] --- .github/workflows/test.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/test.yml b/.github/workflows/test.yml index f58347730..d5625faad 100644 --- a/.github/workflows/test.yml +++ b/.github/workflows/test.yml @@ -70,7 +70,7 @@ jobs: path: target/llvm-cov/html - name: SonarCloud Scan - uses: SonarSource/sonarqube-scan-action@7006c4492b2e0ee0f816d36501671557c97f5995 + uses: SonarSource/sonarqube-scan-action@713881670b6b3676cda39549040e2d88c70d582e if: ${{ env.SONAR_TOKEN }} env: GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} # Needed to get PR information, if any From f345aba695e4382faa3c7dc7ed6389b0ed37f994 Mon Sep 17 00:00:00 2001 From: Eugene Date: Wed, 10 Jun 2026 21:59:11 +0200 Subject: [PATCH 175/556] bump schemars --- Cargo.lock | 12 ++++++------ Cargo.toml | 2 +- config-schema.json | 48 +++++++++++++++++++++++----------------------- 3 files changed, 31 insertions(+), 31 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index 1d7e6c253..4a2b04880 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -5718,7 +5718,6 @@ checksum = "4cd191f9397d57d581cddd31014772520aa448f65ef991055d7f61582c65165f" dependencies = [ "dyn-clone", "ref-cast", - "schemars_derive", "serde", "serde_json", ] @@ -5731,15 +5730,16 @@ checksum = "a2b42f36aa1cd011945615b92222f6bf73c599a102a300334cd7f8dbeec726cc" dependencies = [ "dyn-clone", "ref-cast", + "schemars_derive", "serde", "serde_json", ] [[package]] name = "schemars_derive" -version = "0.9.0" +version = "1.2.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5016d94c77c6d32f0b8e08b781f7dc8a90c2007d4e77472cc2807bc10a8438fe" +checksum = "7d115b50f4aaeea07e79c1912f645c7513d81715d0420f8bc77a18c6260b307f" dependencies = [ "proc-macro2", "quote", @@ -7607,7 +7607,7 @@ dependencies = [ "rcgen", "reqwest 0.13.4", "rustls 0.23.40", - "schemars 0.9.0", + "schemars 1.2.1", "sd-notify", "sea-orm", "serde_json", @@ -7741,7 +7741,7 @@ dependencies = [ "rustls 0.23.40", "rustls-native-certs", "rustls-pki-types", - "schemars 0.9.0", + "schemars 1.2.1", "sea-orm", "serde", "serde_json", @@ -8067,7 +8067,7 @@ dependencies = [ "jsonwebtoken", "openidconnect", "reqwest 0.12.28", - "schemars 0.9.0", + "schemars 1.2.1", "serde", "serde_json", "subtle", diff --git a/Cargo.toml b/Cargo.toml index 67a93ed75..23c4f5e95 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -87,7 +87,7 @@ password-hash = { version = "0.5", features = [ delegate = { version = "0.13", default-features = false } subtle = { version = "2", default-features = false } tracing = { version = "0.1", default-features = false } -schemars = { version = "0.9.0", default-features = false, features = [ +schemars = { version = "1.2", default-features = false, features = [ "derive", "std", ] } diff --git a/config-schema.json b/config-schema.json index 09ea7b8a3..17e3cf669 100644 --- a/config-schema.json +++ b/config-schema.json @@ -43,8 +43,8 @@ "log": { "$ref": "#/$defs/LogConfig", "default": { - "format": "text", "audit_retention": "11months 30days 3h 50m 24s", + "format": "text", "retention": "7days", "send_to": null } @@ -220,14 +220,14 @@ "LogConfig": { "type": "object", "properties": { - "format": { - "$ref": "#/$defs/LogFormat", - "default": "text" - }, "audit_retention": { "type": "string", "default": "11months 30days 3h 50m 24s" }, + "format": { + "$ref": "#/$defs/LogFormat", + "default": "text" + }, "retention": { "type": "string", "default": "7days" @@ -338,7 +338,7 @@ } }, "RoleMapping": { - "description": "A role mapping value that accepts either a single role or a list of roles.\n In YAML config: `\"group\": \"role\"` or `\"group\": [\"role1\", \"role2\"]`", + "description": "A role mapping value that accepts either a single role or a list of roles.\nIn YAML config: `\"group\": \"role\"` or `\"group\": [\"role1\", \"role2\"]`", "anyOf": [ { "type": "string" @@ -432,10 +432,6 @@ { "type": "object", "properties": { - "type": { - "type": "string", - "const": "google" - }, "admin_email": { "description": "A Google Workspace admin email for domain-wide delegation", "type": [ @@ -459,7 +455,7 @@ "type": "string" }, "role_mappings": { - "description": "Maps Google group email addresses to Warpgate role names.\n Use \"*\" as a key to set a default role for any group not explicitly mapped.", + "description": "Maps Google group email addresses to Warpgate role names.\nUse \"*\" as a key to set a default role for any group not explicitly mapped.", "type": [ "object", "null" @@ -481,6 +477,10 @@ "string", "null" ] + }, + "type": { + "type": "string", + "const": "google" } }, "required": [ @@ -492,10 +492,6 @@ { "type": "object", "properties": { - "type": { - "type": "string", - "const": "apple" - }, "client_id": { "type": "string" }, @@ -507,6 +503,10 @@ }, "team_id": { "type": "string" + }, + "type": { + "type": "string", + "const": "apple" } }, "required": [ @@ -520,10 +520,6 @@ { "type": "object", "properties": { - "type": { - "type": "string", - "const": "azure" - }, "client_id": { "type": "string" }, @@ -532,6 +528,10 @@ }, "tenant": { "type": "string" + }, + "type": { + "type": "string", + "const": "azure" } }, "required": [ @@ -544,10 +544,6 @@ { "type": "object", "properties": { - "type": { - "type": "string", - "const": "custom" - }, "additional_trusted_audiences": { "type": [ "array", @@ -593,6 +589,10 @@ "trust_unknown_audiences": { "type": "boolean", "default": false + }, + "type": { + "type": "string", + "const": "custom" } }, "required": [ @@ -613,7 +613,7 @@ "default": false }, "default_credential_policy": { - "description": "Default credential policy for auto-created users.\n Keys: \"http\", \"ssh\", \"mysql\", \"postgres\"\n Values: list of credential kinds e.g. [\"sso\"], [\"web\"], []" + "description": "Default credential policy for auto-created users.\nKeys: \"http\", \"ssh\", \"mysql\", \"postgres\"\nValues: list of credential kinds e.g. [\"sso\"], [\"web\"], []" }, "label": { "type": [ From 55614ce92cdcb641c081501bde9fb7c8811cf918 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Wed, 10 Jun 2026 23:53:57 +0200 Subject: [PATCH 176/556] Bump ratatui from 0.29.0 to 0.30.0 (#2042) Signed-off-by: dependabot[bot] --- Cargo.lock | 270 +++++++++++++++++++++++++++++-- warpgate-protocol-ssh/Cargo.toml | 2 +- 2 files changed, 262 insertions(+), 10 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index 4a2b04880..8ab487094 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -166,6 +166,15 @@ version = "1.0.102" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "7f202df86484c868dbad7eaa557ef785d5c66295e41b460ef922eca0723b842c" +[[package]] +name = "approx" +version = "0.5.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cab112f0a86d568ea0e627cc1d6be74a1e9cd55214684db5561995f6dad897c6" +dependencies = [ + "num-traits", +] + [[package]] name = "arc-swap" version = "1.9.1" @@ -1049,6 +1058,12 @@ version = "3.20.3" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "72f5acc6cb2ba439de613abc23857ec3d78374d8ed5ac84e9d11336e87da8649" +[[package]] +name = "by_address" +version = "1.2.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "64fa3c856b712db6612c019f14756e64e4bcea13337a6b33b696333a9eaa2d06" + [[package]] name = "bytemuck" version = "1.25.0" @@ -1259,6 +1274,20 @@ dependencies = [ "static_assertions", ] +[[package]] +name = "compact_str" +version = "0.9.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9dfdd1c2274d9aa354115b09dc9a901d6c5576818cdf70d14cae2bdb47df00ab" +dependencies = [ + "castaway", + "cfg-if", + "itoa", + "rustversion", + "ryu", + "static_assertions", +] + [[package]] name = "compression-codecs" version = "0.4.38" @@ -1498,6 +1527,24 @@ dependencies = [ "winapi", ] +[[package]] +name = "crossterm" +version = "0.29.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d8b9f2e4c67f833b660cdb0a3523065869fb35570177239812ed4c905aeff87b" +dependencies = [ + "bitflags 2.13.0", + "crossterm_winapi", + "derive_more", + "document-features", + "mio", + "parking_lot", + "rustix 1.1.4", + "signal-hook", + "signal-hook-mio", + "winapi", +] + [[package]] name = "crossterm_winapi" version = "0.9.1" @@ -1925,6 +1972,15 @@ dependencies = [ "syn 2.0.117", ] +[[package]] +name = "document-features" +version = "0.2.12" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d4b8a88685455ed29a21542a33abd9cb6510b6b129abadabdcef0f4c55bc8f61" +dependencies = [ + "litrs", +] + [[package]] name = "dotenvy" version = "0.15.7" @@ -2165,6 +2221,12 @@ dependencies = [ "regex", ] +[[package]] +name = "fast-srgb8" +version = "1.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "dd2e7510819d6fbf51a5545c8f922716ecfb14df168a3242f7d33e0239efe6a1" + [[package]] name = "fastrand" version = "2.4.1" @@ -2638,6 +2700,11 @@ name = "hashbrown" version = "0.17.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "ed5909b6e89a2db4456e54cd5f673791d7eca6732202bbf2a9cc504fe2f9b84a" +dependencies = [ + "allocator-api2", + "equivalent", + "foldhash 0.2.0", +] [[package]] name = "hashlink" @@ -3360,6 +3427,17 @@ dependencies = [ "zeroize", ] +[[package]] +name = "kasuari" +version = "0.4.12" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bde5057d6143cc94e861d90f591b9303d6716c6b9602309150bd068853c10899" +dependencies = [ + "hashbrown 0.16.1", + "portable-atomic", + "thiserror 2.0.18", +] + [[package]] name = "keccak" version = "0.2.0" @@ -3525,6 +3603,15 @@ dependencies = [ "vcpkg", ] +[[package]] +name = "line-clipping" +version = "0.3.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3f50e8f47623268b5407192d26876c4d7f89d686ca130fdc53bced4814cd29f8" +dependencies = [ + "bitflags 2.13.0", +] + [[package]] name = "linux-raw-sys" version = "0.4.15" @@ -3543,6 +3630,12 @@ version = "0.8.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "92daf443525c4cce67b150400bc2316076100ce0b3686209eb8cf3c31612e6f0" +[[package]] +name = "litrs" +version = "1.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "11d3d7f243d5c5a8b9bb5d6dd2b1602c0cb0b9db1621bafc7ed66e35ff9fe092" + [[package]] name = "lock_api" version = "0.4.14" @@ -3567,6 +3660,15 @@ dependencies = [ "hashbrown 0.15.5", ] +[[package]] +name = "lru" +version = "0.18.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8a860605968fce16869fd239cf4237a82f3ac470723415db603b0e8b6c8d4fb9" +dependencies = [ + "hashbrown 0.17.1", +] + [[package]] name = "mac_address" version = "1.1.8" @@ -3951,6 +4053,12 @@ dependencies = [ "libc", ] +[[package]] +name = "numtoa" +version = "0.2.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6aa2c4e539b869820a2b82e1aef6ff40aa85e65decdd5185e83fb4b1249cd00f" + [[package]] name = "oauth2" version = "5.0.0" @@ -4180,6 +4288,30 @@ dependencies = [ "windows-strings", ] +[[package]] +name = "palette" +version = "0.7.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4cbf71184cc5ecc2e4e1baccdb21026c20e5fc3dcf63028a086131b3ab00b6e6" +dependencies = [ + "approx", + "fast-srgb8", + "libm", + "palette_derive", +] + +[[package]] +name = "palette_derive" +version = "0.7.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f5030daf005bface118c096f510ffb781fc28f9ab6a32ab224d8631be6851d30" +dependencies = [ + "by_address", + "proc-macro2", + "quote", + "syn 2.0.117", +] + [[package]] name = "parking" version = "2.2.1" @@ -5052,16 +5184,94 @@ checksum = "eabd94c2f37801c20583fc49dd5cd6b0ba68c716787c2dd6ed18571e1e63117b" dependencies = [ "bitflags 2.13.0", "cassowary", - "compact_str", - "crossterm", + "compact_str 0.8.2", + "crossterm 0.28.1", "indoc", "instability", "itertools 0.13.0", - "lru", + "lru 0.12.5", "paste", - "strum", + "strum 0.26.3", + "unicode-segmentation", + "unicode-truncate 1.1.0", + "unicode-width 0.2.0", +] + +[[package]] +name = "ratatui" +version = "0.30.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d1ce67fb8ba4446454d1c8dbaeda0557ff5e94d39d5e5ed7f10a65eb4c8266bc" +dependencies = [ + "instability", + "ratatui-core", + "ratatui-crossterm", + "ratatui-termion", + "ratatui-widgets", +] + +[[package]] +name = "ratatui-core" +version = "0.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "42d3603f354bba8c595fa47860e60142d7372b7210c27044c6a7d0e1a4336b44" +dependencies = [ + "bitflags 2.13.0", + "compact_str 0.9.1", + "hashbrown 0.17.1", + "indoc", + "itertools 0.14.0", + "kasuari", + "lru 0.18.0", + "palette", + "serde", + "strum 0.28.0", + "thiserror 2.0.18", + "unicode-segmentation", + "unicode-truncate 2.0.1", + "unicode-width 0.2.0", +] + +[[package]] +name = "ratatui-crossterm" +version = "0.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2b2867bedcbd6a690ca4f8672a687b730ec07660c79844517b084311b529980c" +dependencies = [ + "cfg-if", + "crossterm 0.29.0", + "instability", + "ratatui-core", +] + +[[package]] +name = "ratatui-termion" +version = "0.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5c16cc35a9d9114e0b2bb4b22018b96ae7f5fe60e2595dc73e622b4e78624835" +dependencies = [ + "instability", + "ratatui-core", + "termion", +] + +[[package]] +name = "ratatui-widgets" +version = "0.3.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7ef4f17dd7ac3abf5adc2b920a03c61eee4bfe6a88fa5191936895525371d79c" +dependencies = [ + "bitflags 2.13.0", + "hashbrown 0.17.1", + "indoc", + "instability", + "itertools 0.14.0", + "line-clipping", + "ratatui-core", + "serde", + "strum 0.28.0", + "time", "unicode-segmentation", - "unicode-truncate", "unicode-width 0.2.0", ] @@ -5820,7 +6030,7 @@ dependencies = [ "serde", "serde_json", "sqlx", - "strum", + "strum 0.26.3", "thiserror 2.0.18", "time", "tracing", @@ -6728,7 +6938,16 @@ version = "0.26.3" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "8fec0f0aef304996cf250b31b5a10dee7980c85da9d759361292b8bca5a18f06" dependencies = [ - "strum_macros", + "strum_macros 0.26.4", +] + +[[package]] +name = "strum" +version = "0.28.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9628de9b8791db39ceda2b119bbe13134770b56c138ec1d3af810d045c04f9bd" +dependencies = [ + "strum_macros 0.28.0", ] [[package]] @@ -6744,6 +6963,18 @@ dependencies = [ "syn 2.0.117", ] +[[package]] +name = "strum_macros" +version = "0.28.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ab85eea0270ee17587ed4156089e10b9e6880ee688791d45a905f5b1ca36f664" +dependencies = [ + "heck 0.5.0", + "proc-macro2", + "quote", + "syn 2.0.117", +] + [[package]] name = "subprocess" version = "0.2.15" @@ -6837,6 +7068,16 @@ dependencies = [ "phf_codegen", ] +[[package]] +name = "termion" +version = "4.0.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f44138a9ae08f0f502f24104d82517ef4da7330c35acd638f1f29d3cd5475ecb" +dependencies = [ + "libc", + "numtoa", +] + [[package]] name = "termios" version = "0.3.3" @@ -7319,7 +7560,7 @@ version = "0.14.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "911e93158bf80bbc94bad533b2b16e3d711e1132d69a6a6980c3920a63422c19" dependencies = [ - "ratatui", + "ratatui 0.29.0", "unicode-width 0.2.0", ] @@ -7430,6 +7671,17 @@ dependencies = [ "unicode-width 0.1.14", ] +[[package]] +name = "unicode-truncate" +version = "2.0.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "16b380a1238663e5f8a691f9039c73e1cdae598a30e9855f541d29b08b53e9a5" +dependencies = [ + "itertools 0.14.0", + "unicode-segmentation", + "unicode-width 0.2.0", +] + [[package]] name = "unicode-width" version = "0.1.14" @@ -8036,7 +8288,7 @@ dependencies = [ "dialoguer", "ed25519-dalek 2.2.0", "futures", - "ratatui", + "ratatui 0.30.0", "russh", "sea-orm", "serde", diff --git a/warpgate-protocol-ssh/Cargo.toml b/warpgate-protocol-ssh/Cargo.toml index 9b7aa1f30..588914b96 100644 --- a/warpgate-protocol-ssh/Cargo.toml +++ b/warpgate-protocol-ssh/Cargo.toml @@ -14,7 +14,7 @@ curve25519-dalek = { version = "4.0.0", default-features = false } # pin due to dialoguer.workspace = true ed25519-dalek = { version = "2.0.0", default-features = false } # pin due to build fail on x86 in 2.1 futures.workspace = true -ratatui = { version = "0.29", default-features = false, features = [ +ratatui = { version = "0.30", default-features = false, features = [ "crossterm", "unstable-backend-writer", ] } From d58cf153b4c6a45f3409f8f668482b339b1ad80e Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Wed, 10 Jun 2026 23:54:36 +0200 Subject: [PATCH 177/556] Bump termwiz from 0.22.0 to 0.23.3 (#2041) Signed-off-by: dependabot[bot] --- Cargo.lock | 103 +++++-------------------------- warpgate-protocol-ssh/Cargo.toml | 2 +- 2 files changed, 16 insertions(+), 89 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index 8ab487094..ecc249e89 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -1941,26 +1941,6 @@ dependencies = [ "ctutils", ] -[[package]] -name = "dirs" -version = "4.0.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ca3aa72a6f96ea37bbc5aa912f6788242832f75369bdfdadcb0e38423f100059" -dependencies = [ - "dirs-sys", -] - -[[package]] -name = "dirs-sys" -version = "0.3.7" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1b1d1d91c932ef41c0f2663aa8b0ca0342d444d842c06914aa0a7e352d0bada6" -dependencies = [ - "libc", - "redox_users", - "winapi", -] - [[package]] name = "displaydoc" version = "0.2.6" @@ -3733,15 +3713,6 @@ version = "0.1.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "a64a92489e2744ce060c349162be1c5f33c6969234104dbd99ddb5feb08b8c15" -[[package]] -name = "memoffset" -version = "0.7.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5de893c32cde5f383baa4c04c5d6dbdd735cfd4a794b0debdb2bb1b421da5ff4" -dependencies = [ - "autocfg", -] - [[package]] name = "memoffset" version = "0.9.1" @@ -3869,19 +3840,6 @@ dependencies = [ "zstd", ] -[[package]] -name = "nix" -version = "0.26.4" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "598beaf3cc6fdd9a5dfb1630c2800c7acd31df7aaf0f565796fba2b53ca1af1b" -dependencies = [ - "bitflags 1.3.2", - "cfg-if", - "libc", - "memoffset 0.7.1", - "pin-utils", -] - [[package]] name = "nix" version = "0.29.0" @@ -3892,7 +3850,7 @@ dependencies = [ "cfg-if", "cfg_aliases", "libc", - "memoffset 0.9.1", + "memoffset", ] [[package]] @@ -4005,13 +3963,13 @@ checksum = "521739c6d2bac4aa25192232afe6841231376b2b26d4d9fae5ecf8ca5772e441" [[package]] name = "num-derive" -version = "0.3.3" +version = "0.4.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "876a53fff98e03a936a674b29568b0e605f06b29372c2489ff4de23f1949743d" +checksum = "ed3955f1a9c7c0c15e092f9c887db08b1fc683305fdf6eb6684f22555355e202" dependencies = [ "proc-macro2", "quote", - "syn 1.0.109", + "syn 2.0.117", ] [[package]] @@ -5317,17 +5275,6 @@ dependencies = [ "bitflags 2.13.0", ] -[[package]] -name = "redox_users" -version = "0.4.6" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ba009ff324d1fc1b900bd1fdb31564febe58a8ccc8a6fdbb93b543d33b13ca43" -dependencies = [ - "getrandom 0.2.17", - "libredox", - "thiserror 1.0.69", -] - [[package]] name = "ref-cast" version = "1.0.25" @@ -5729,7 +5676,7 @@ version = "0.4.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "cfcb3a22ef46e85b45de6ee7e79d063319ebb6594faafcf1c225ea92ab6e9b92" dependencies = [ - "semver 1.0.28", + "semver", ] [[package]] @@ -6214,30 +6161,12 @@ dependencies = [ "libc", ] -[[package]] -name = "semver" -version = "0.11.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f301af10236f6df4160f7c3f04eec6dbc70ace82d23326abad5edee88801c6b6" -dependencies = [ - "semver-parser", -] - [[package]] name = "semver" version = "1.0.28" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "8a7852d02fc848982e0c167ef163aaff9cd91dc640ba85e263cb1ce46fae51cd" -[[package]] -name = "semver-parser" -version = "0.10.3" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9900206b54a3527fdc7b8a938bffd94a568bac4f4aa8113b209df75a09c0dec2" -dependencies = [ - "pest", -] - [[package]] name = "serde" version = "1.0.228" @@ -7057,11 +6986,10 @@ dependencies = [ [[package]] name = "terminfo" -version = "0.8.0" +version = "0.9.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "666cd3a6681775d22b200409aad3b089c5b99fb11ecdd8a204d9d62f8148498f" +checksum = "d4ea810f0692f9f51b382fff5893887bb4580f5fa246fde546e0b13e7fcee662" dependencies = [ - "dirs", "fnv", "nom", "phf 0.11.3", @@ -7089,12 +7017,12 @@ dependencies = [ [[package]] name = "termwiz" -version = "0.22.0" +version = "0.23.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5a75313e21da5d4406ea31402035b3b97aa74c04356bdfafa5d1043ab4e551d1" +checksum = "4676b37242ccbd1aabf56edb093a4827dc49086c0ffd764a5705899e0f35f8f7" dependencies = [ "anyhow", - "base64 0.21.7", + "base64 0.22.1", "bitflags 2.13.0", "fancy-regex", "filedescriptor", @@ -7105,18 +7033,16 @@ dependencies = [ "libc", "log", "memmem", - "nix 0.26.4", + "nix 0.29.0", "num-derive", "num-traits", "ordered-float 4.6.0", "pest", "pest_derive", "phf 0.11.3", - "semver 0.11.0", "sha2 0.10.9", "signal-hook", - "siphasher 0.3.11", - "tempfile", + "siphasher 1.0.3", "terminfo", "termios", "thiserror 1.0.69", @@ -8527,7 +8453,7 @@ dependencies = [ "bitflags 2.13.0", "hashbrown 0.15.5", "indexmap 2.14.0", - "semver 1.0.28", + "semver", ] [[package]] @@ -8655,6 +8581,7 @@ dependencies = [ "bitflags 1.3.2", "euclid", "lazy_static", + "serde", "wezterm-dynamic", ] @@ -9141,7 +9068,7 @@ dependencies = [ "id-arena", "indexmap 2.14.0", "log", - "semver 1.0.28", + "semver", "serde", "serde_derive", "serde_json", diff --git a/warpgate-protocol-ssh/Cargo.toml b/warpgate-protocol-ssh/Cargo.toml index 588914b96..52198c488 100644 --- a/warpgate-protocol-ssh/Cargo.toml +++ b/warpgate-protocol-ssh/Cargo.toml @@ -22,7 +22,7 @@ russh.workspace = true sea-orm.workspace = true serde.workspace = true termcolor = { version = "1", default-features = false } -termwiz = { version = "0.22", default-features = false } +termwiz = { version = "0.23", default-features = false } thiserror.workspace = true time = { version = "0.3", default-features = false } tokio.workspace = true From 226aa112a145ebdbfabe2be0b506a3a3e8ffc2b3 Mon Sep 17 00:00:00 2001 From: Eugene Date: Wed, 10 Jun 2026 23:57:54 +0200 Subject: [PATCH 178/556] fixe #2038 - saving a jump host option resets it (#2043) --- .cargo/config.toml | 7 +++++++ Cargo.toml | 13 ++++++------- rust-toolchain.toml | 2 +- .../src/admin/config/targets/ssh/Options.svelte | 2 +- 4 files changed, 15 insertions(+), 9 deletions(-) diff --git a/.cargo/config.toml b/.cargo/config.toml index 8ab422546..611a4779f 100644 --- a/.cargo/config.toml +++ b/.cargo/config.toml @@ -6,3 +6,10 @@ rustflags = [ "--remap-path-prefix=$HOME=/reproducible-home", "--remap-path-prefix=$PWD=/reproducible-pwd", ] + +[unstable] +profile-hint-mostly-unused = true +cargo-lints = true + +[lints.cargo] +implicit-features = "warn" diff --git a/Cargo.toml b/Cargo.toml index 23c4f5e95..74862d1a0 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -158,22 +158,21 @@ strip = "debuginfo" [profile.coverage] inherits = "dev" -# rustflags = ["-Cinstrument-coverage"] [profile.dev.package.aws-sdk-ec2] -rustflags = ["-Zhint-mostly-unused"] +hint-mostly-unused = true [profile.release.package.aws-sdk-ec2] -rustflags = ["-Zhint-mostly-unused"] +hint-mostly-unused = true [profile.dev.package.aws-sdk-rds] -rustflags = ["-Zhint-mostly-unused"] +hint-mostly-unused = true [profile.release.package.aws-sdk-rds] -rustflags = ["-Zhint-mostly-unused"] +hint-mostly-unused = true [profile.dev.package.aws-sdk-eks] -rustflags = ["-Zhint-mostly-unused"] +hint-mostly-unused = true [profile.release.package.aws-sdk-eks] -rustflags = ["-Zhint-mostly-unused"] +hint-mostly-unused = true diff --git a/rust-toolchain.toml b/rust-toolchain.toml index 148280912..d80e1e40c 100644 --- a/rust-toolchain.toml +++ b/rust-toolchain.toml @@ -1,2 +1,2 @@ [toolchain] -channel = "nightly-2025-10-21" +channel = "nightly-2025-12-21" diff --git a/warpgate-web/src/admin/config/targets/ssh/Options.svelte b/warpgate-web/src/admin/config/targets/ssh/Options.svelte index 68eac5323..f4f909b68 100644 --- a/warpgate-web/src/admin/config/targets/ssh/Options.svelte +++ b/warpgate-web/src/admin/config/targets/ssh/Options.svelte @@ -30,7 +30,7 @@ sshTargets = targets.filter(t => t.options.kind === TargetKind.Ssh && t.id !== id) }) - let jumpHostSelectValue = $state('') + let jumpHostSelectValue = $state(options.jumpHost ?? '') $effect(() => { const val = jumpHostSelectValue From 5c1d0614689b7783e864fe63dfe3ad5da3d5fc9e Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Wed, 10 Jun 2026 23:58:19 +0200 Subject: [PATCH 179/556] Bump tokio-tungstenite from 0.27.0 to 0.29.0 (#2036) Signed-off-by: dependabot[bot] --- Cargo.lock | 42 +++++++++++++++++++++++++++++++++++------- Cargo.toml | 2 +- 2 files changed, 36 insertions(+), 8 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index ecc249e89..7e8b40aa0 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -4706,7 +4706,7 @@ dependencies = [ "tokio", "tokio-rustls 0.26.4", "tokio-stream", - "tokio-tungstenite", + "tokio-tungstenite 0.27.0", "tokio-util", "tracing", "wildmatch", @@ -7249,6 +7249,18 @@ name = "tokio-tungstenite" version = "0.27.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "489a59b6730eda1b0171fcfda8b121f4bee2b35cba8645ca35c5f7ba3eb736c1" +dependencies = [ + "futures-util", + "log", + "tokio", + "tungstenite 0.27.0", +] + +[[package]] +name = "tokio-tungstenite" +version = "0.29.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8f72a05e828585856dacd553fba484c242c46e391fb0e58917c942ee9202915c" dependencies = [ "futures-util", "log", @@ -7257,7 +7269,7 @@ dependencies = [ "rustls-pki-types", "tokio", "tokio-rustls 0.26.4", - "tungstenite 0.27.0", + "tungstenite 0.29.0", ] [[package]] @@ -7502,8 +7514,6 @@ dependencies = [ "httparse", "log", "rand 0.9.4", - "rustls 0.23.40", - "rustls-pki-types", "sha1 0.10.6", "thiserror 2.0.18", "utf-8", @@ -7526,6 +7536,24 @@ dependencies = [ "utf-8", ] +[[package]] +name = "tungstenite" +version = "0.29.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6c01152af293afb9c7c2a57e4b559c5620b421f6d133261c60dd2d0cdb38e6b8" +dependencies = [ + "bytes", + "data-encoding", + "http 1.4.1", + "httparse", + "log", + "rand 0.9.4", + "rustls 0.23.40", + "rustls-pki-types", + "sha1 0.10.6", + "thiserror 2.0.18", +] + [[package]] name = "typenum" version = "1.20.1" @@ -7928,7 +7956,7 @@ dependencies = [ "tokio", "tokio-rustls 0.26.4", "tokio-stream", - "tokio-tungstenite", + "tokio-tungstenite 0.29.0", "totp-rs", "tracing", "tracing-core", @@ -8091,7 +8119,7 @@ dependencies = [ "subtle", "time", "tokio", - "tokio-tungstenite", + "tokio-tungstenite 0.29.0", "tracing", "url", "uuid", @@ -8134,7 +8162,7 @@ dependencies = [ "time", "tokio", "tokio-rustls 0.26.4", - "tokio-tungstenite", + "tokio-tungstenite 0.29.0", "tracing", "url", "uuid", diff --git a/Cargo.toml b/Cargo.toml index 74862d1a0..55c8b8006 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -143,7 +143,7 @@ reqwest_12 = { package = "reqwest", version = "0.12", features = [ "gzip", ], default-features = false } # separate copy to control features on openidconnect->oauth2->reqwest regex = { version = "1.6", default-features = false, features = ["std"] } -tokio-tungstenite = { version = "0.27", features = [ +tokio-tungstenite = { version = "0.29", features = [ "rustls-tls-native-roots", "connect", ], default-features = false } From fc3ddcf5e9dab0f59ce4910f8aa1cdcb62bcdab5 Mon Sep 17 00:00:00 2001 From: Eugene Date: Thu, 11 Jun 2026 00:08:32 +0200 Subject: [PATCH 180/556] run typechecking alongside build --- package-lock.json | 274 ++++++++++++++++++ package.json | 5 + warpgate-web/package-lock.json | 185 ------------ warpgate-web/package.json | 7 +- .../admin/config/targets/ssh/Options.svelte | 1 + warpgate-web/vite.config.ts | 2 - 6 files changed, 284 insertions(+), 190 deletions(-) create mode 100644 package-lock.json create mode 100644 package.json diff --git a/package-lock.json b/package-lock.json new file mode 100644 index 000000000..44331a5ca --- /dev/null +++ b/package-lock.json @@ -0,0 +1,274 @@ +{ + "name": "warpgate", + "lockfileVersion": 3, + "requires": true, + "packages": { + "": { + "devDependencies": { + "concurrently": "^10.0.3" + } + }, + "node_modules/ansi-regex": { + "version": "6.2.2", + "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-6.2.2.tgz", + "integrity": "sha512-Bq3SmSpyFHaWjPk8If9yc6svM8c56dB5BAtW4Qbw5jHTwwXXcTLoRMkpDJp6VL0XzlWaCHTXrkFURMYmD0sLqg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/chalk/ansi-regex?sponsor=1" + } + }, + "node_modules/ansi-styles": { + "version": "6.2.3", + "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-6.2.3.tgz", + "integrity": "sha512-4Dj6M28JB+oAH8kFkTLUo+a2jwOFkuqb3yucU0CANcRRUbxS0cP0nZYCGjcc3BNXwRIsUVmDGgzawme7zvJHvg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/chalk/ansi-styles?sponsor=1" + } + }, + "node_modules/chalk": { + "version": "5.6.2", + "resolved": "https://registry.npmjs.org/chalk/-/chalk-5.6.2.tgz", + "integrity": "sha512-7NzBL0rN6fMUW+f7A6Io4h40qQlG+xGmtMxfbnH/K7TAtt8JQWVQK+6g0UXKMeVJoyV5EkkNsErQ8pVD3bLHbA==", + "dev": true, + "license": "MIT", + "engines": { + "node": "^12.17.0 || ^14.13 || >=16.0.0" + }, + "funding": { + "url": "https://github.com/chalk/chalk?sponsor=1" + } + }, + "node_modules/cliui": { + "version": "9.0.1", + "resolved": "https://registry.npmjs.org/cliui/-/cliui-9.0.1.tgz", + "integrity": "sha512-k7ndgKhwoQveBL+/1tqGJYNz097I7WOvwbmmU2AR5+magtbjPWQTS1C5vzGkBC8Ym8UWRzfKUzUUqFLypY4Q+w==", + "dev": true, + "license": "ISC", + "dependencies": { + "string-width": "^7.2.0", + "strip-ansi": "^7.1.0", + "wrap-ansi": "^9.0.0" + }, + "engines": { + "node": ">=20" + } + }, + "node_modules/concurrently": { + "version": "10.0.3", + "resolved": "https://registry.npmjs.org/concurrently/-/concurrently-10.0.3.tgz", + "integrity": "sha512-hc3LH4UaKWd/bbyDK/IGVa4RB6PtQ3CUYwtrkzqHn+wIG3Hr5fhpRlk0L/gCa8ZE1L/Ufj50Zho69cI5w8SQBA==", + "dev": true, + "license": "MIT", + "dependencies": { + "chalk": "5.6.2", + "rxjs": "7.8.2", + "shell-quote": "1.8.4", + "supports-color": "10.2.2", + "tree-kill": "1.2.2", + "yargs": "18.0.0" + }, + "bin": { + "conc": "dist/bin/index.js", + "concurrently": "dist/bin/index.js" + }, + "engines": { + "node": ">=22" + }, + "funding": { + "url": "https://github.com/open-cli-tools/concurrently?sponsor=1" + } + }, + "node_modules/emoji-regex": { + "version": "10.6.0", + "resolved": "https://registry.npmjs.org/emoji-regex/-/emoji-regex-10.6.0.tgz", + "integrity": "sha512-toUI84YS5YmxW219erniWD0CIVOo46xGKColeNQRgOzDorgBi1v4D71/OFzgD9GO2UGKIv1C3Sp8DAn0+j5w7A==", + "dev": true, + "license": "MIT" + }, + "node_modules/escalade": { + "version": "3.2.0", + "resolved": "https://registry.npmjs.org/escalade/-/escalade-3.2.0.tgz", + "integrity": "sha512-WUj2qlxaQtO4g6Pq5c29GTcWGDyd8itL8zTlipgECz3JesAiiOKotd8JU6otB3PACgG6xkJUyVhboMS+bje/jA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6" + } + }, + "node_modules/get-caller-file": { + "version": "2.0.5", + "resolved": "https://registry.npmjs.org/get-caller-file/-/get-caller-file-2.0.5.tgz", + "integrity": "sha512-DyFP3BM/3YHTQOCUL/w0OZHR0lpKeGrxotcHWcqNEdnltqFwXVfhEBQ94eIo34AfQpo0rGki4cyIiftY06h2Fg==", + "dev": true, + "license": "ISC", + "engines": { + "node": "6.* || 8.* || >= 10.*" + } + }, + "node_modules/get-east-asian-width": { + "version": "1.6.0", + "resolved": "https://registry.npmjs.org/get-east-asian-width/-/get-east-asian-width-1.6.0.tgz", + "integrity": "sha512-QRbvDIbx6YklUe6RxeTeleMR0yv3cYH6PsPZHcnVn7xv7zO1BHN8r0XETu8n6Ye3Q+ahtSarc3WgtNWmehIBfA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=18" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/rxjs": { + "version": "7.8.2", + "resolved": "https://registry.npmjs.org/rxjs/-/rxjs-7.8.2.tgz", + "integrity": "sha512-dhKf903U/PQZY6boNNtAGdWbG85WAbjT/1xYoZIC7FAY0yWapOBQVsVrDl58W86//e1VpMNBtRV4MaXfdMySFA==", + "dev": true, + "license": "Apache-2.0", + "dependencies": { + "tslib": "^2.1.0" + } + }, + "node_modules/shell-quote": { + "version": "1.8.4", + "resolved": "https://registry.npmjs.org/shell-quote/-/shell-quote-1.8.4.tgz", + "integrity": "sha512-VsC6n6vz1ihYYyZZwX7YZSF5l5x36ca17OC+a69h94YqB7X6XLwf+5MOgynYir2SLFUbl8gIYvBo8K8RoNQ6bQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/string-width": { + "version": "7.2.0", + "resolved": "https://registry.npmjs.org/string-width/-/string-width-7.2.0.tgz", + "integrity": "sha512-tsaTIkKW9b4N+AEj+SVA+WhJzV7/zMhcSu78mLKWSk7cXMOSHsBKFWUs0fWwq8QyK3MgJBQRX6Gbi4kYbdvGkQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "emoji-regex": "^10.3.0", + "get-east-asian-width": "^1.0.0", + "strip-ansi": "^7.1.0" + }, + "engines": { + "node": ">=18" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/strip-ansi": { + "version": "7.2.0", + "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-7.2.0.tgz", + "integrity": "sha512-yDPMNjp4WyfYBkHnjIRLfca1i6KMyGCtsVgoKe/z1+6vukgaENdgGBZt+ZmKPc4gavvEZ5OgHfHdrazhgNyG7w==", + "dev": true, + "license": "MIT", + "dependencies": { + "ansi-regex": "^6.2.2" + }, + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/chalk/strip-ansi?sponsor=1" + } + }, + "node_modules/supports-color": { + "version": "10.2.2", + "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-10.2.2.tgz", + "integrity": "sha512-SS+jx45GF1QjgEXQx4NJZV9ImqmO2NPz5FNsIHrsDjh2YsHnawpan7SNQ1o8NuhrbHZy9AZhIoCUiCeaW/C80g==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=18" + }, + "funding": { + "url": "https://github.com/chalk/supports-color?sponsor=1" + } + }, + "node_modules/tree-kill": { + "version": "1.2.2", + "resolved": "https://registry.npmjs.org/tree-kill/-/tree-kill-1.2.2.tgz", + "integrity": "sha512-L0Orpi8qGpRG//Nd+H90vFB+3iHnue1zSSGmNOOCh1GLJ7rUKVwV2HvijphGQS2UmhUZewS9VgvxYIdgr+fG1A==", + "dev": true, + "license": "MIT", + "bin": { + "tree-kill": "cli.js" + } + }, + "node_modules/tslib": { + "version": "2.8.1", + "resolved": "https://registry.npmjs.org/tslib/-/tslib-2.8.1.tgz", + "integrity": "sha512-oJFu94HQb+KVduSUQL7wnpmqnfmLsOA/nAh6b6EH0wCEoK0/mPeXU6c3wKDV83MkOuHPRHtSXKKU99IBazS/2w==", + "dev": true, + "license": "0BSD" + }, + "node_modules/wrap-ansi": { + "version": "9.0.2", + "resolved": "https://registry.npmjs.org/wrap-ansi/-/wrap-ansi-9.0.2.tgz", + "integrity": "sha512-42AtmgqjV+X1VpdOfyTGOYRi0/zsoLqtXQckTmqTeybT+BDIbM/Guxo7x3pE2vtpr1ok6xRqM9OpBe+Jyoqyww==", + "dev": true, + "license": "MIT", + "dependencies": { + "ansi-styles": "^6.2.1", + "string-width": "^7.0.0", + "strip-ansi": "^7.1.0" + }, + "engines": { + "node": ">=18" + }, + "funding": { + "url": "https://github.com/chalk/wrap-ansi?sponsor=1" + } + }, + "node_modules/y18n": { + "version": "5.0.8", + "resolved": "https://registry.npmjs.org/y18n/-/y18n-5.0.8.tgz", + "integrity": "sha512-0pfFzegeDWJHJIAmTLRP2DwHjdF5s7jo9tuztdQxAhINCdvS+3nGINqPd00AphqJR/0LhANUS6/+7SCb98YOfA==", + "dev": true, + "license": "ISC", + "engines": { + "node": ">=10" + } + }, + "node_modules/yargs": { + "version": "18.0.0", + "resolved": "https://registry.npmjs.org/yargs/-/yargs-18.0.0.tgz", + "integrity": "sha512-4UEqdc2RYGHZc7Doyqkrqiln3p9X2DZVxaGbwhn2pi7MrRagKaOcIKe8L3OxYcbhXLgLFUS3zAYuQjKBQgmuNg==", + "dev": true, + "license": "MIT", + "dependencies": { + "cliui": "^9.0.1", + "escalade": "^3.1.1", + "get-caller-file": "^2.0.5", + "string-width": "^7.2.0", + "y18n": "^5.0.5", + "yargs-parser": "^22.0.0" + }, + "engines": { + "node": "^20.19.0 || ^22.12.0 || >=23" + } + }, + "node_modules/yargs-parser": { + "version": "22.0.0", + "resolved": "https://registry.npmjs.org/yargs-parser/-/yargs-parser-22.0.0.tgz", + "integrity": "sha512-rwu/ClNdSMpkSrUb+d6BRsSkLUq1fmfsY6TOpYzTwvwkg1/NRG85KBy3kq++A8LKQwX6lsu+aWad+2khvuXrqw==", + "dev": true, + "license": "ISC", + "engines": { + "node": "^20.19.0 || ^22.12.0 || >=23" + } + } + } +} diff --git a/package.json b/package.json new file mode 100644 index 000000000..489dfd87d --- /dev/null +++ b/package.json @@ -0,0 +1,5 @@ +{ + "devDependencies": { + "concurrently": "^10.0.3" + } +} diff --git a/warpgate-web/package-lock.json b/warpgate-web/package-lock.json index e9809e5ef..45b365589 100644 --- a/warpgate-web/package-lock.json +++ b/warpgate-web/package-lock.json @@ -59,36 +59,10 @@ "typescript-eslint": "^8.57.1", "ua-parser-js": "^2.0.9", "vite": "^7.3.1", - "vite-plugin-checker": "^0.12.0", "vite-tsconfig-paths": "^6.1.1", "zmodem.js": "^0.1.10" } }, - "node_modules/@babel/code-frame": { - "version": "7.27.1", - "resolved": "https://registry.npmjs.org/@babel/code-frame/-/code-frame-7.27.1.tgz", - "integrity": "sha512-cjQ7ZlQ0Mv3b47hABuTevyTuYN4i+loJKGeV9flcCgIK37cCXRh+L1bd3iBHlynerhQ7BhCkn2BPbQUL+rGqFg==", - "dev": true, - "license": "MIT", - "dependencies": { - "@babel/helper-validator-identifier": "^7.27.1", - "js-tokens": "^4.0.0", - "picocolors": "^1.1.1" - }, - "engines": { - "node": ">=6.9.0" - } - }, - "node_modules/@babel/helper-validator-identifier": { - "version": "7.27.1", - "resolved": "https://registry.npmjs.org/@babel/helper-validator-identifier/-/helper-validator-identifier-7.27.1.tgz", - "integrity": "sha512-D2hP9eA+Sqx1kBZgzxZh0y1trbuU+JoDkiEwqhQ36nodYqJwyEIhPSdMNd7lOm/4io72luTPWH20Yda0xOuUow==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=6.9.0" - } - }, "node_modules/@borewit/text-codec": { "version": "0.2.2", "resolved": "https://registry.npmjs.org/@borewit/text-codec/-/text-codec-0.2.2.tgz", @@ -5588,13 +5562,6 @@ "node": ">=6" } }, - "node_modules/js-tokens": { - "version": "4.0.0", - "resolved": "https://registry.npmjs.org/js-tokens/-/js-tokens-4.0.0.tgz", - "integrity": "sha512-RdJUflcE3cUzKiMqQgsCu06FPu9UdIJO0beYbPhHN4k6apgJtifcoCtT9bcxOpYBtpD2kCM6Sbzg4CausW/PKQ==", - "dev": true, - "license": "MIT" - }, "node_modules/js-yaml": { "version": "4.1.1", "resolved": "https://registry.npmjs.org/js-yaml/-/js-yaml-4.1.1.tgz", @@ -5944,36 +5911,6 @@ "node": ">=0.10.0" } }, - "node_modules/npm-run-path": { - "version": "6.0.0", - "resolved": "https://registry.npmjs.org/npm-run-path/-/npm-run-path-6.0.0.tgz", - "integrity": "sha512-9qny7Z9DsQU8Ou39ERsPU4OZQlSTP47ShQzuKZ6PRXpYLtIFgl/DEBYEXKlvcEa+9tHVcK8CF81Y2V72qaZhWA==", - "dev": true, - "license": "MIT", - "dependencies": { - "path-key": "^4.0.0", - "unicorn-magic": "^0.3.0" - }, - "engines": { - "node": ">=18" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } - }, - "node_modules/npm-run-path/node_modules/path-key": { - "version": "4.0.0", - "resolved": "https://registry.npmjs.org/path-key/-/path-key-4.0.0.tgz", - "integrity": "sha512-haREypq7xkM7ErfgIyA0z+Bj4AGKlMSdlQE2jvJo6huWD1EdkKYV+G/T4nq0YEF2vgTT8kqMFKo1uHn950r4SQ==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=12" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } - }, "node_modules/object-inspect": { "version": "1.13.4", "resolved": "https://registry.npmjs.org/object-inspect/-/object-inspect-1.13.4.tgz", @@ -7599,13 +7536,6 @@ "dev": true, "license": "Apache-2.0" }, - "node_modules/tiny-invariant": { - "version": "1.3.3", - "resolved": "https://registry.npmjs.org/tiny-invariant/-/tiny-invariant-1.3.3.tgz", - "integrity": "sha512-+FbBPE1o9QAYvviau/qC5SE3caw21q3xkvWKBtja5vgqOWIHHJ3ioaq1VPfn/Szqctz2bU/oYeKd9/z5BL+PVg==", - "dev": true, - "license": "MIT" - }, "node_modules/tinyglobby": { "version": "0.2.15", "resolved": "https://registry.npmjs.org/tinyglobby/-/tinyglobby-0.2.15.tgz", @@ -7981,19 +7911,6 @@ "dev": true, "license": "MIT" }, - "node_modules/unicorn-magic": { - "version": "0.3.0", - "resolved": "https://registry.npmjs.org/unicorn-magic/-/unicorn-magic-0.3.0.tgz", - "integrity": "sha512-+QBBXBCvifc56fsbuxZQ6Sic3wqqc3WWaqxs58gvJrcOuN83HGTCwz3oS5phzU9LthRNE9VrJCFCLUgHeeFnfA==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=18" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } - }, "node_modules/universalify": { "version": "2.0.1", "resolved": "https://registry.npmjs.org/universalify/-/universalify-2.0.1.tgz", @@ -8132,101 +8049,6 @@ } } }, - "node_modules/vite-plugin-checker": { - "version": "0.12.0", - "resolved": "https://registry.npmjs.org/vite-plugin-checker/-/vite-plugin-checker-0.12.0.tgz", - "integrity": "sha512-CmdZdDOGss7kdQwv73UyVgLPv0FVYe5czAgnmRX2oKljgEvSrODGuClaV3PDR2+3ou7N/OKGauDDBjy2MB07Rg==", - "dev": true, - "license": "MIT", - "dependencies": { - "@babel/code-frame": "^7.27.1", - "chokidar": "^4.0.3", - "npm-run-path": "^6.0.0", - "picocolors": "^1.1.1", - "picomatch": "^4.0.3", - "tiny-invariant": "^1.3.3", - "tinyglobby": "^0.2.15", - "vscode-uri": "^3.1.0" - }, - "engines": { - "node": ">=16.11" - }, - "peerDependencies": { - "@biomejs/biome": ">=1.7", - "eslint": ">=9.39.1", - "meow": "^13.2.0", - "optionator": "^0.9.4", - "oxlint": ">=1", - "stylelint": ">=16", - "typescript": "*", - "vite": ">=5.4.21", - "vls": "*", - "vti": "*", - "vue-tsc": "~2.2.10 || ^3.0.0" - }, - "peerDependenciesMeta": { - "@biomejs/biome": { - "optional": true - }, - "eslint": { - "optional": true - }, - "meow": { - "optional": true - }, - "optionator": { - "optional": true - }, - "oxlint": { - "optional": true - }, - "stylelint": { - "optional": true - }, - "typescript": { - "optional": true - }, - "vls": { - "optional": true - }, - "vti": { - "optional": true - }, - "vue-tsc": { - "optional": true - } - } - }, - "node_modules/vite-plugin-checker/node_modules/chokidar": { - "version": "4.0.3", - "resolved": "https://registry.npmjs.org/chokidar/-/chokidar-4.0.3.tgz", - "integrity": "sha512-Qgzu8kfBvo+cA4962jnP1KkS6Dop5NS6g7R5LFYJr4b8Ub94PPQXUksCw9PvXoeXPRRddRNC5C1JQUR2SMGtnA==", - "dev": true, - "license": "MIT", - "dependencies": { - "readdirp": "^4.0.1" - }, - "engines": { - "node": ">= 14.16.0" - }, - "funding": { - "url": "https://paulmillr.com/funding/" - } - }, - "node_modules/vite-plugin-checker/node_modules/readdirp": { - "version": "4.1.2", - "resolved": "https://registry.npmjs.org/readdirp/-/readdirp-4.1.2.tgz", - "integrity": "sha512-GDhwkLfywWL2s6vEjyhri+eXmfH6j1L7JE27WhqLeYzoh/A3DBaYGEj2H/HFZCn/kMfim73FXxEJTw06WtxQwg==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">= 14.18.0" - }, - "funding": { - "type": "individual", - "url": "https://paulmillr.com/funding/" - } - }, "node_modules/vite-tsconfig-paths": { "version": "6.1.1", "resolved": "https://registry.npmjs.org/vite-tsconfig-paths/-/vite-tsconfig-paths-6.1.1.tgz", @@ -8262,13 +8084,6 @@ } } }, - "node_modules/vscode-uri": { - "version": "3.1.0", - "resolved": "https://registry.npmjs.org/vscode-uri/-/vscode-uri-3.1.0.tgz", - "integrity": "sha512-/BpdSx+yCQGnCvecbyXdxHDkuk55/G3xwnC0GqY4gmQ3j+A+g8kzzgB4Nk/SINjqn6+waqw3EgbVF2QKExkRxQ==", - "dev": true, - "license": "MIT" - }, "node_modules/wcwidth": { "version": "1.0.1", "resolved": "https://registry.npmjs.org/wcwidth/-/wcwidth-1.0.1.tgz", diff --git a/warpgate-web/package.json b/warpgate-web/package.json index 970c06cb0..64891ab93 100644 --- a/warpgate-web/package.json +++ b/warpgate-web/package.json @@ -4,9 +4,11 @@ "version": "0.0.0", "type": "module", "scripts": { - "build": "vite build", + "build": "npm run check && vite build", "devbuild": "NODE_ENV=development vite build --mode development --minify false", - "watch": "NODE_ENV=development vite build -w --mode development --minify false", + "watch": "concurrently -k -n vite,check -c blue,yellow npm:watch:vite npm:watch:check", + "watch:vite": "NODE_ENV=development vite build -w --mode development --minify false", + "watch:check": "npm run check -- --watch", "check": "svelte-check --compiler-warnings 'a11y-no-noninteractive-element-interactions:ignore,a11y-click-events-have-key-events:ignore,a11y-no-static-element-interactions:ignore' --tsconfig ./tsconfig.json", "lint": "eslint src && svelte-check", "postinstall": "npm run openapi:client:gateway && npm run openapi:client:admin", @@ -64,7 +66,6 @@ "typescript-eslint": "^8.57.1", "ua-parser-js": "^2.0.9", "vite": "^7.3.1", - "vite-plugin-checker": "^0.12.0", "vite-tsconfig-paths": "^6.1.1", "zmodem.js": "^0.1.10" }, diff --git a/warpgate-web/src/admin/config/targets/ssh/Options.svelte b/warpgate-web/src/admin/config/targets/ssh/Options.svelte index f4f909b68..b48105c5a 100644 --- a/warpgate-web/src/admin/config/targets/ssh/Options.svelte +++ b/warpgate-web/src/admin/config/targets/ssh/Options.svelte @@ -30,6 +30,7 @@ sshTargets = targets.filter(t => t.options.kind === TargetKind.Ssh && t.id !== id) }) + // svelte-ignore state_referenced_locally let jumpHostSelectValue = $state(options.jumpHost ?? '') $effect(() => { diff --git a/warpgate-web/vite.config.ts b/warpgate-web/vite.config.ts index e7843c049..48da52d73 100644 --- a/warpgate-web/vite.config.ts +++ b/warpgate-web/vite.config.ts @@ -1,14 +1,12 @@ import { defineConfig } from 'vite' import { svelte } from '@sveltejs/vite-plugin-svelte' import tsconfigPaths from 'vite-tsconfig-paths' -import { checker } from 'vite-plugin-checker' // https://vitejs.dev/config/ export default defineConfig({ plugins: [ svelte(), tsconfigPaths(), - // checker({ typescript: true }), ], base: '/@warpgate', build: { From 239aea139670b0e6e2775798124c84fdd00df7e0 Mon Sep 17 00:00:00 2001 From: Eugene Date: Thu, 11 Jun 2026 00:16:08 +0200 Subject: [PATCH 181/556] =?UTF-8?q?fixed=20#2005=20-=20prefer=20external?= =?UTF-8?q?=5Fhosts.http=20as=20return=20host=20in=20the=20embedd=E2=80=A6?= =?UTF-8?q?=20(#2044)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- warpgate-web/src/embed/EmbeddedUI.svelte | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/warpgate-web/src/embed/EmbeddedUI.svelte b/warpgate-web/src/embed/EmbeddedUI.svelte index 9b8cb1909..1bb3991cd 100644 --- a/warpgate-web/src/embed/EmbeddedUI.svelte +++ b/warpgate-web/src/embed/EmbeddedUI.svelte @@ -19,7 +19,7 @@ if (localStorage.warpgateMenuLocation) { onMount(async () => { ready = true const info = await api.getInfo() - externalHost = info.externalHost + externalHost = `${info.externalHosts?.http ?? info.externalHost}:${info.ports.http ?? 443}` }) function drag (e: MouseEvent) { From c4cc2f7de0952de0e3a32e5405219dd5a063d2aa Mon Sep 17 00:00:00 2001 From: Eugene Date: Thu, 11 Jun 2026 00:16:30 +0200 Subject: [PATCH 182/556] bump version to 0.25.3 --- bumpver.toml | 2 +- helm/warpgate/Chart.yaml | 2 +- helm/warpgate/values.yaml | 2 +- warpgate-admin/Cargo.toml | 2 +- warpgate-ca/Cargo.toml | 2 +- warpgate-common-http/Cargo.toml | 2 +- warpgate-common/Cargo.toml | 2 +- warpgate-core/Cargo.toml | 2 +- warpgate-database-protocols/Cargo.toml | 2 +- warpgate-db-entities/Cargo.toml | 2 +- warpgate-db-migrations/Cargo.toml | 2 +- warpgate-ldap/Cargo.toml | 2 +- warpgate-protocol-http/Cargo.toml | 2 +- warpgate-protocol-kubernetes/Cargo.toml | 2 +- warpgate-protocol-mysql/Cargo.toml | 2 +- warpgate-protocol-postgres/Cargo.toml | 2 +- warpgate-protocol-ssh/Cargo.toml | 2 +- warpgate-sso/Cargo.toml | 2 +- warpgate-tls/Cargo.toml | 2 +- warpgate-web/Cargo.toml | 2 +- warpgate/Cargo.toml | 2 +- 21 files changed, 21 insertions(+), 21 deletions(-) diff --git a/bumpver.toml b/bumpver.toml index e147e9692..2c57b8a1b 100644 --- a/bumpver.toml +++ b/bumpver.toml @@ -1,5 +1,5 @@ [bumpver] -current_version = "0.25.2" +current_version = "0.25.3" version_pattern = "MAJOR.MINOR.PATCH[-TAG[.INC0]]" commit = true tag = false diff --git a/helm/warpgate/Chart.yaml b/helm/warpgate/Chart.yaml index 92a5097a4..6835250b5 100644 --- a/helm/warpgate/Chart.yaml +++ b/helm/warpgate/Chart.yaml @@ -22,4 +22,4 @@ version: 0.0.2 # incremented each time you make changes to the application. Versions are not expected to # follow Semantic Versioning. They should reflect the version the application is using. # It is recommended to use it with quotes. -appVersion: "0.25.2" +appVersion: "0.25.3" diff --git a/helm/warpgate/values.yaml b/helm/warpgate/values.yaml index d996a5142..c616257e7 100644 --- a/helm/warpgate/values.yaml +++ b/helm/warpgate/values.yaml @@ -5,7 +5,7 @@ replicaCount: 1 image: repository: ghcr.io/warp-tech/warpgate pullPolicy: IfNotPresent - tag: "0.25.2" + tag: "0.25.3" # References to Kubernetes secrets for pulling images (if using a private registry) imagePullSecrets: [] diff --git a/warpgate-admin/Cargo.toml b/warpgate-admin/Cargo.toml index 63f715809..4bb565dc7 100644 --- a/warpgate-admin/Cargo.toml +++ b/warpgate-admin/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "warpgate-admin" -version = "0.25.2" +version = "0.25.3" edition = "2024" license = "Apache-2.0" publish = false diff --git a/warpgate-ca/Cargo.toml b/warpgate-ca/Cargo.toml index ca74a63f0..4509fb6b4 100644 --- a/warpgate-ca/Cargo.toml +++ b/warpgate-ca/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "warpgate-ca" -version = "0.25.2" +version = "0.25.3" edition = "2024" license = "Apache-2.0" publish = false diff --git a/warpgate-common-http/Cargo.toml b/warpgate-common-http/Cargo.toml index e24981343..c3ac4ee06 100644 --- a/warpgate-common-http/Cargo.toml +++ b/warpgate-common-http/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "warpgate-common-http" -version = "0.25.2" +version = "0.25.3" edition = "2024" license = "Apache-2.0" publish = false diff --git a/warpgate-common/Cargo.toml b/warpgate-common/Cargo.toml index e3f3c0b66..6b6c7da5a 100644 --- a/warpgate-common/Cargo.toml +++ b/warpgate-common/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "warpgate-common" -version = "0.25.2" +version = "0.25.3" edition = "2024" license = "Apache-2.0" publish = false diff --git a/warpgate-core/Cargo.toml b/warpgate-core/Cargo.toml index 750b5fc4e..6dc4f7d47 100644 --- a/warpgate-core/Cargo.toml +++ b/warpgate-core/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "warpgate-core" -version = "0.25.2" +version = "0.25.3" edition = "2024" license = "Apache-2.0" publish = false diff --git a/warpgate-database-protocols/Cargo.toml b/warpgate-database-protocols/Cargo.toml index f7cd6943e..682d7246a 100644 --- a/warpgate-database-protocols/Cargo.toml +++ b/warpgate-database-protocols/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "warpgate-database-protocols" -version = "0.25.2" +version = "0.25.3" description = "Core of SQLx, the rust SQL toolkit. Just the database protocol parts." authors = [ "Ryan Leckey ", diff --git a/warpgate-db-entities/Cargo.toml b/warpgate-db-entities/Cargo.toml index 5ad8f1fc8..08534c131 100644 --- a/warpgate-db-entities/Cargo.toml +++ b/warpgate-db-entities/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "warpgate-db-entities" -version = "0.25.2" +version = "0.25.3" edition = "2024" license = "Apache-2.0" publish = false diff --git a/warpgate-db-migrations/Cargo.toml b/warpgate-db-migrations/Cargo.toml index 00ac1b7f2..996befba9 100644 --- a/warpgate-db-migrations/Cargo.toml +++ b/warpgate-db-migrations/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "warpgate-db-migrations" -version = "0.25.2" +version = "0.25.3" edition = "2024" license = "Apache-2.0" publish = false diff --git a/warpgate-ldap/Cargo.toml b/warpgate-ldap/Cargo.toml index 7fa65ba11..7063243c3 100644 --- a/warpgate-ldap/Cargo.toml +++ b/warpgate-ldap/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "warpgate-ldap" -version = "0.25.2" +version = "0.25.3" edition = "2024" license = "Apache-2.0" publish = false diff --git a/warpgate-protocol-http/Cargo.toml b/warpgate-protocol-http/Cargo.toml index abf41a65c..ccca8f844 100644 --- a/warpgate-protocol-http/Cargo.toml +++ b/warpgate-protocol-http/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "warpgate-protocol-http" -version = "0.25.2" +version = "0.25.3" edition = "2024" license = "Apache-2.0" publish = false diff --git a/warpgate-protocol-kubernetes/Cargo.toml b/warpgate-protocol-kubernetes/Cargo.toml index aec1748b2..89dcf4012 100644 --- a/warpgate-protocol-kubernetes/Cargo.toml +++ b/warpgate-protocol-kubernetes/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "warpgate-protocol-kubernetes" -version = "0.25.2" +version = "0.25.3" edition = "2024" license = "Apache-2.0" publish = false diff --git a/warpgate-protocol-mysql/Cargo.toml b/warpgate-protocol-mysql/Cargo.toml index 8dca74099..991014548 100644 --- a/warpgate-protocol-mysql/Cargo.toml +++ b/warpgate-protocol-mysql/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "warpgate-protocol-mysql" -version = "0.25.2" +version = "0.25.3" edition = "2024" license = "Apache-2.0" publish = false diff --git a/warpgate-protocol-postgres/Cargo.toml b/warpgate-protocol-postgres/Cargo.toml index baec2d657..5fc3677a6 100644 --- a/warpgate-protocol-postgres/Cargo.toml +++ b/warpgate-protocol-postgres/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "warpgate-protocol-postgres" -version = "0.25.2" +version = "0.25.3" edition = "2024" license = "Apache-2.0" publish = false diff --git a/warpgate-protocol-ssh/Cargo.toml b/warpgate-protocol-ssh/Cargo.toml index 52198c488..bf147a564 100644 --- a/warpgate-protocol-ssh/Cargo.toml +++ b/warpgate-protocol-ssh/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "warpgate-protocol-ssh" -version = "0.25.2" +version = "0.25.3" edition = "2024" license = "Apache-2.0" publish = false diff --git a/warpgate-sso/Cargo.toml b/warpgate-sso/Cargo.toml index 7fecec849..3e935b317 100644 --- a/warpgate-sso/Cargo.toml +++ b/warpgate-sso/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "warpgate-sso" -version = "0.25.2" +version = "0.25.3" edition = "2024" license = "Apache-2.0" publish = false diff --git a/warpgate-tls/Cargo.toml b/warpgate-tls/Cargo.toml index 8242aebf2..13a42e9aa 100644 --- a/warpgate-tls/Cargo.toml +++ b/warpgate-tls/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "warpgate-tls" -version = "0.25.2" +version = "0.25.3" edition = "2024" license = "Apache-2.0" publish = false diff --git a/warpgate-web/Cargo.toml b/warpgate-web/Cargo.toml index 68ec5f95c..18a2d9647 100644 --- a/warpgate-web/Cargo.toml +++ b/warpgate-web/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "warpgate-web" -version = "0.25.2" +version = "0.25.3" edition = "2024" license = "Apache-2.0" diff --git a/warpgate/Cargo.toml b/warpgate/Cargo.toml index d0d412788..2be5c66cf 100644 --- a/warpgate/Cargo.toml +++ b/warpgate/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "warpgate" -version = "0.25.2" +version = "0.25.3" edition = "2024" license = "Apache-2.0" publish = false From 75d8a63e65247072c01105863b25e5c5d64d7b2d Mon Sep 17 00:00:00 2001 From: Eugene Date: Thu, 11 Jun 2026 00:16:38 +0200 Subject: [PATCH 183/556] Update Cargo.lock --- Cargo.lock | 36 ++++++++++++++++++------------------ 1 file changed, 18 insertions(+), 18 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index 7e8b40aa0..065f7bf12 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -7796,7 +7796,7 @@ dependencies = [ [[package]] name = "warpgate" -version = "0.25.2" +version = "0.25.3" dependencies = [ "anyhow", "async-trait", @@ -7839,7 +7839,7 @@ dependencies = [ [[package]] name = "warpgate-admin" -version = "0.25.2" +version = "0.25.3" dependencies = [ "anyhow", "async-trait", @@ -7899,7 +7899,7 @@ dependencies = [ [[package]] name = "warpgate-ca" -version = "0.25.2" +version = "0.25.3" dependencies = [ "aws-lc-rs", "bytes", @@ -7920,7 +7920,7 @@ dependencies = [ [[package]] name = "warpgate-common" -version = "0.25.2" +version = "0.25.3" dependencies = [ "anyhow", "argon2 0.5.3", @@ -7973,7 +7973,7 @@ dependencies = [ [[package]] name = "warpgate-common-http" -version = "0.25.2" +version = "0.25.3" dependencies = [ "poem", "poem-openapi", @@ -7988,7 +7988,7 @@ dependencies = [ [[package]] name = "warpgate-core" -version = "0.25.2" +version = "0.25.3" dependencies = [ "anyhow", "argon2 0.5.3", @@ -8033,7 +8033,7 @@ dependencies = [ [[package]] name = "warpgate-database-protocols" -version = "0.25.2" +version = "0.25.3" dependencies = [ "bitflags 2.13.0", "bytes", @@ -8046,7 +8046,7 @@ dependencies = [ [[package]] name = "warpgate-db-entities" -version = "0.25.2" +version = "0.25.3" dependencies = [ "bytes", "ipnet", @@ -8065,7 +8065,7 @@ dependencies = [ [[package]] name = "warpgate-db-migrations" -version = "0.25.2" +version = "0.25.3" dependencies = [ "data-encoding", "regex", @@ -8083,7 +8083,7 @@ dependencies = [ [[package]] name = "warpgate-ldap" -version = "0.25.2" +version = "0.25.3" dependencies = [ "anyhow", "ldap3", @@ -8099,7 +8099,7 @@ dependencies = [ [[package]] name = "warpgate-protocol-http" -version = "0.25.2" +version = "0.25.3" dependencies = [ "anyhow", "async-trait", @@ -8138,7 +8138,7 @@ dependencies = [ [[package]] name = "warpgate-protocol-kubernetes" -version = "0.25.2" +version = "0.25.3" dependencies = [ "anyhow", "async-trait", @@ -8177,7 +8177,7 @@ dependencies = [ [[package]] name = "warpgate-protocol-mysql" -version = "0.25.2" +version = "0.25.3" dependencies = [ "anyhow", "async-trait", @@ -8205,7 +8205,7 @@ dependencies = [ [[package]] name = "warpgate-protocol-postgres" -version = "0.25.2" +version = "0.25.3" dependencies = [ "anyhow", "async-trait", @@ -8232,7 +8232,7 @@ dependencies = [ [[package]] name = "warpgate-protocol-ssh" -version = "0.25.2" +version = "0.25.3" dependencies = [ "anyhow", "async-trait", @@ -8265,7 +8265,7 @@ dependencies = [ [[package]] name = "warpgate-sso" -version = "0.25.2" +version = "0.25.3" dependencies = [ "bytes", "data-encoding", @@ -8285,7 +8285,7 @@ dependencies = [ [[package]] name = "warpgate-tls" -version = "0.25.2" +version = "0.25.3" dependencies = [ "poem", "poem-openapi", @@ -8305,7 +8305,7 @@ dependencies = [ [[package]] name = "warpgate-web" -version = "0.25.2" +version = "0.25.3" dependencies = [ "rust-embed", "serde", From f8bf56b714d0d82c64d20aad516c85c972c2b797 Mon Sep 17 00:00:00 2001 From: Eugene Date: Thu, 11 Jun 2026 10:39:55 +0200 Subject: [PATCH 184/556] fixed #2039 - allow setting Postgres protocol version explicitly (#2047) --- warpgate-common/src/config/target.rs | 16 +++++++++++++++- warpgate-protocol-postgres/src/session.rs | 9 +++++++-- .../src/admin/config/targets/Target.svelte | 13 +++++++++++++ warpgate-web/src/admin/lib/openapi-schema.json | 12 +++++++++++- warpgate-web/src/gateway/lib/openapi-schema.json | 2 +- 5 files changed, 47 insertions(+), 5 deletions(-) diff --git a/warpgate-common/src/config/target.rs b/warpgate-common/src/config/target.rs index ae2b5b4a8..79b907cbe 100644 --- a/warpgate-common/src/config/target.rs +++ b/warpgate-common/src/config/target.rs @@ -1,6 +1,6 @@ use std::collections::HashMap; -use poem_openapi::{Object, Union}; +use poem_openapi::{Enum, Object, Union}; use serde::{Deserialize, Serialize}; use uuid::Uuid; use warpgate_tls::TlsMode; @@ -188,6 +188,17 @@ impl TargetMySqlOptions { } } +#[derive(Debug, Deserialize, Serialize, Clone, Copy, PartialEq, Eq, Default, Enum)] +pub enum PostgresProtocolVersion { + #[serde(rename = "3.0")] + #[oai(rename = "3.0")] + V3_0, + #[default] + #[serde(rename = "3.2")] + #[oai(rename = "3.2")] + V3_2, +} + #[derive(Debug, Deserialize, Serialize, Clone, Object)] pub struct TargetPostgresOptions { #[serde(default = "_default_empty_string")] @@ -215,6 +226,9 @@ pub struct TargetPostgresOptions { #[serde(default)] pub default_database_name: Option, + + #[serde(default)] + pub protocol_version: Option, } impl TargetPostgresOptions { diff --git a/warpgate-protocol-postgres/src/session.rs b/warpgate-protocol-postgres/src/session.rs index a52fb05bf..17f57884a 100644 --- a/warpgate-protocol-postgres/src/session.rs +++ b/warpgate-protocol-postgres/src/session.rs @@ -19,7 +19,7 @@ use uuid::Uuid; use warpgate_common::auth::{ AuthCredential, AuthResult, AuthSelector, AuthStateUserInfo, CredentialKind, }; -use warpgate_common::{Secret, TargetOptions, TargetPostgresOptions}; +use warpgate_common::{PostgresProtocolVersion, Secret, TargetOptions, TargetPostgresOptions}; use warpgate_common_http::ext::construct_external_url; use warpgate_core::auth::validate_and_add_credential; use warpgate_core::{ @@ -414,10 +414,15 @@ impl PostgresSession { startup: pgwire::messages::startup::Startup, options: TargetPostgresOptions, ) -> Result<(), PostgresError> { + let target_protocol_version = match options.protocol_version.unwrap_or_default() { + PostgresProtocolVersion::V3_0 => ProtocolVersion::PROTOCOL3_0, + PostgresProtocolVersion::V3_2 => ProtocolVersion::PROTOCOL3_2, + }; + let mut client = match PostgresClient::connect( &options, ConnectionOptions { - protocol_version: ProtocolVersion::PROTOCOL3_2, + protocol_version: target_protocol_version, parameters: startup.parameters, }, ) diff --git a/warpgate-web/src/admin/config/targets/Target.svelte b/warpgate-web/src/admin/config/targets/Target.svelte index 537bdefdf..9ff0df21c 100644 --- a/warpgate-web/src/admin/config/targets/Target.svelte +++ b/warpgate-web/src/admin/config/targets/Target.svelte @@ -38,6 +38,9 @@ api.getTarget({ id: params.id }), api.listTargetGroups(), ]) + if (target.options.kind === 'Postgres') { + target.options.protocolVersion ??= '3.2' + } } async function loadRoles () { @@ -354,6 +357,16 @@ {#if target.options.kind === 'MySql' || target.options.kind === 'Postgres'}
{#if target.options.kind === 'Postgres'} + + + + Postgres protocol version for the target connection. You might have to choose 3.0 here for some non-compliant Postgres proxies that do not implement automatic version negotiation. + + + Date: Thu, 11 Jun 2026 10:46:03 +0200 Subject: [PATCH 185/556] Bump mysql_common from 0.34.1 to 0.37.2 (#2040) Signed-off-by: dependabot[bot] --- Cargo.lock | 55 ++++-------------------------- warpgate-protocol-mysql/Cargo.toml | 2 +- 2 files changed, 7 insertions(+), 50 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index 065f7bf12..80479ebe0 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -1045,9 +1045,9 @@ dependencies = [ [[package]] name = "btoi" -version = "0.4.3" +version = "0.5.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9dd6407f73a9b8b6162d8a2ef999fe6afd7cc15902ebf42c5cd296addf17e0ad" +checksum = "3b5ab9db53bcda568284df0fd39f6eac24ad6f7ba7ff1168b9e76eba6576b976" dependencies = [ "num-traits", ] @@ -3811,33 +3811,28 @@ dependencies = [ [[package]] name = "mysql_common" -version = "0.34.1" +version = "0.37.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "34a9141e735d5bb02414a7ac03add09522466d4db65bdd827069f76ae0850e58" +checksum = "4b42ced54aa8ac97226486337973f9bc3956e24f03a23e88a6e18f640959d6e2" dependencies = [ "base64 0.22.1", "bitflags 2.13.0", "btoi", "byteorder", "bytes", - "cc", - "cmake", "crc32fast", "flate2", - "lazy_static", + "getrandom 0.3.4", "num-bigint", "num-traits", - "rand 0.8.6", "regex", "saturating", "serde", "serde_json", "sha1 0.10.6", "sha2 0.10.9", - "subprocess", - "thiserror 1.0.69", + "thiserror 2.0.18", "uuid", - "zstd", ] [[package]] @@ -6904,16 +6899,6 @@ dependencies = [ "syn 2.0.117", ] -[[package]] -name = "subprocess" -version = "0.2.15" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "2c56e8662b206b9892d7a5a3f2ecdbcb455d3d6b259111373b7e08b8055158a8" -dependencies = [ - "libc", - "winapi", -] - [[package]] name = "subtle" version = "2.6.1" @@ -9341,31 +9326,3 @@ name = "zmij" version = "1.0.21" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "b8848ee67ecc8aedbaf3e4122217aff892639231befc6a1b58d29fff4c2cabaa" - -[[package]] -name = "zstd" -version = "0.13.3" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e91ee311a569c327171651566e07972200e76fcfe2242a4fa446149a3881c08a" -dependencies = [ - "zstd-safe", -] - -[[package]] -name = "zstd-safe" -version = "7.2.4" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8f49c4d5f0abb602a93fb8736af2a4f4dd9512e36f7f570d66e65ff867ed3b9d" -dependencies = [ - "zstd-sys", -] - -[[package]] -name = "zstd-sys" -version = "2.0.16+zstd.1.5.7" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "91e19ebc2adc8f83e43039e79776e3fda8ca919132d68a1fed6a5faca2683748" -dependencies = [ - "cc", - "pkg-config", -] diff --git a/warpgate-protocol-mysql/Cargo.toml b/warpgate-protocol-mysql/Cargo.toml index 991014548..88b3429a3 100644 --- a/warpgate-protocol-mysql/Cargo.toml +++ b/warpgate-protocol-mysql/Cargo.toml @@ -11,7 +11,7 @@ async-trait.workspace = true bytes.workspace = true flate2 = { version = "1", features = ["zlib"], default-features = false } futures.workspace = true -mysql_common = { version = "0.34", default-features = false } +mysql_common = { version = "0.37", default-features = false } password-hash.workspace = true rand.workspace = true rustls.workspace = true From 7fa7c6e8c095054d502137458fa868172a7d3ed5 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Thu, 11 Jun 2026 10:46:11 +0200 Subject: [PATCH 186/556] Bump the version-bumps group across 1 directory with 12 updates (#2045) Signed-off-by: dependabot[bot] --- warpgate-web/package-lock.json | 609 ++++++++++++++++++--------------- warpgate-web/package.json | 24 +- 2 files changed, 345 insertions(+), 288 deletions(-) diff --git a/warpgate-web/package-lock.json b/warpgate-web/package-lock.json index 45b365589..6804c0e40 100644 --- a/warpgate-web/package-lock.json +++ b/warpgate-web/package-lock.json @@ -9,7 +9,7 @@ "version": "0.0.0", "hasInstallScript": true, "dependencies": { - "@tanstack/svelte-virtual": "^3.13.23", + "@tanstack/svelte-virtual": "^3.13.28", "natural-orderby": "^5.0.0" }, "devDependencies": { @@ -18,7 +18,7 @@ "@fortawesome/free-brands-svg-icons": "^7.2.0", "@fortawesome/free-regular-svg-icons": "^7.2.0", "@fortawesome/free-solid-svg-icons": "^7.2.0", - "@openapitools/openapi-generator-cli": "^2.31.1", + "@openapitools/openapi-generator-cli": "^2.34.0", "@otplib/plugin-base32-enc-dec": "^12.0.1", "@otplib/plugin-crypto-js": "^12.0.1", "@otplib/preset-browser": "^12.0.1", @@ -34,30 +34,30 @@ "@xterm/xterm": "^6.0", "bootstrap": "^5.3.8", "copy-text-to-clipboard": "^3.2.2", - "date-fns": "^4.1.0", + "date-fns": "^4.4.0", "eslint": "^9", - "eslint-import-resolver-typescript": "^4.4.4", + "eslint-import-resolver-typescript": "^4.4.5", "eslint-plugin-import": "^2.32.0", "eslint-plugin-node": "^11.1.0", "eslint-plugin-promise": "^6", - "eslint-plugin-svelte": "^3.15.2", + "eslint-plugin-svelte": "^3.19.0", "format-duration": "^3.0.2", - "otpauth": "^9.5.0", + "otpauth": "^9.5.1", "qrcode": "^1.5.4", "rxjs": "^7.8.2", "sass": "1.78", - "svelte": "^5.55.7", - "svelte-check": "^4.4.5", + "svelte": "^5.56.1", + "svelte-check": "^4.5.0", "svelte-fa": "^4.0.4", "svelte-intersection-observer": "^1.1.1", "svelte-observable": "^0.4.0", - "svelte-preprocess": "^6.0.3", + "svelte-preprocess": "^6.0.5", "svelte-spa-router": "^4.0.1", - "thenby": "^1.3.4", + "thenby": "^1.4.1", "tslib": "^2.8.0", "typescript": "^5.9.3", - "typescript-eslint": "^8.57.1", - "ua-parser-js": "^2.0.9", + "typescript-eslint": "^8.60.1", + "ua-parser-js": "^2.0.10", "vite": "^7.3.1", "vite-tsconfig-paths": "^6.1.1", "zmodem.js": "^0.1.10" @@ -1055,14 +1055,13 @@ } }, "node_modules/@nestjs/common": { - "version": "11.1.17", - "resolved": "https://registry.npmjs.org/@nestjs/common/-/common-11.1.17.tgz", - "integrity": "sha512-hLODw5Abp8OQgA+mUO4tHou4krKgDtUcM9j5Ihxncst9XeyxYBTt2bwZm4e4EQr5E352S4Fyy6V3iFx9ggxKAg==", + "version": "11.1.21", + "resolved": "https://registry.npmjs.org/@nestjs/common/-/common-11.1.21.tgz", + "integrity": "sha512-YV1HYDGsm2rnR0vrLKidtrG6jYX5yqiIjeur1j8++dKGqhhsJ6cjMs0RfQRSTUH7IjgDemA59/znQ8nRrE0D9g==", "dev": true, "license": "MIT", - "peer": true, "dependencies": { - "file-type": "21.3.2", + "file-type": "21.3.4", "iterare": "1.2.1", "load-esm": "1.0.3", "tslib": "2.8.1", @@ -1088,9 +1087,9 @@ } }, "node_modules/@nestjs/core": { - "version": "11.1.18", - "resolved": "https://registry.npmjs.org/@nestjs/core/-/core-11.1.18.tgz", - "integrity": "sha512-wR3DtGyk/LUAiPtbXDuWJJwVkWElKBY0sqnTzf9d4uM3+X18FRZhK7WFc47czsIGOdWuRsMeLYV+1Z9dO4zDEQ==", + "version": "11.1.21", + "resolved": "https://registry.npmjs.org/@nestjs/core/-/core-11.1.21.tgz", + "integrity": "sha512-fqo0BHgny3MOuAL8GSfG3ZUKFVVBaBQD/0iyibnwTONT5vPexjQxJzu+945iloVvBDmrnAaRWxC1gqCDEs/AXQ==", "dev": true, "hasInstallScript": true, "license": "MIT", @@ -1130,9 +1129,9 @@ } }, "node_modules/@noble/hashes": { - "version": "2.0.1", - "resolved": "https://registry.npmjs.org/@noble/hashes/-/hashes-2.0.1.tgz", - "integrity": "sha512-XlOlEbQcE9fmuXxrVTXCTlG2nlRXa9Rj3rr5Ue/+tX+nmkgbX720YHh0VR3hBF9xDvwnb8D2shVGOwNx+ulArw==", + "version": "2.2.0", + "resolved": "https://registry.npmjs.org/@noble/hashes/-/hashes-2.2.0.tgz", + "integrity": "sha512-IYqDGiTXab6FniAgnSdZwgWbomxpy9FtYvLKs7wCUs2a8RkITG+DFGO1DM9cr+E3/RgADRpFjrKVaJ1z6sjtEg==", "dev": true, "license": "MIT", "engines": { @@ -1186,27 +1185,27 @@ "license": "MIT" }, "node_modules/@openapitools/openapi-generator-cli": { - "version": "2.31.1", - "resolved": "https://registry.npmjs.org/@openapitools/openapi-generator-cli/-/openapi-generator-cli-2.31.1.tgz", - "integrity": "sha512-dPE+COjNLLTHFQ1lddUvpo+J8YQB1RD3/NVRJ3K+1hPZnyuxCURgOCmr7mXgHEyHmzWH8dKXWm/pD170iVR0vw==", + "version": "2.34.0", + "resolved": "https://registry.npmjs.org/@openapitools/openapi-generator-cli/-/openapi-generator-cli-2.34.0.tgz", + "integrity": "sha512-Z6400REeiq16xkRrdKgtv8nY3xy0DhUnc42DIT5rWjzoj7l4qn+MwqsOqUVJ5UwOF9VUBQGKN7jrDaEkKfj3kQ==", "dev": true, "hasInstallScript": true, "license": "Apache-2.0", "dependencies": { "@inquirer/select": "1.3.3", "@nestjs/axios": "4.0.1", - "@nestjs/common": "11.1.17", - "@nestjs/core": "11.1.18", + "@nestjs/common": "11.1.21", + "@nestjs/core": "11.1.21", "@nuxtjs/opencollective": "0.3.2", - "axios": "^1.14.0", + "axios": "^1.16.1", "chalk": "4.1.2", "commander": "8.3.0", "compare-versions": "6.1.1", "concurrently": "9.2.1", "console.table": "0.10.0", - "fs-extra": "11.3.4", + "fs-extra": "11.3.5", "glob": "13.0.6", - "proxy-agent": "6.5.0", + "proxy-agent": "8.0.1", "reflect-metadata": "0.2.2", "rxjs": "7.8.2", "tslib": "2.8.1" @@ -1275,7 +1274,6 @@ "integrity": "sha512-P1st0aksCrn9sGZhp8GMYwBnQsbvAWsZAX44oXNNvLHGqAOcoVxmjZiohstwQ7SqKnbR47akdNi+uleWD8+g6A==", "dev": true, "license": "MIT", - "peer": true, "funding": { "type": "opencollective", "url": "https://opencollective.com/popperjs" @@ -1660,9 +1658,9 @@ } }, "node_modules/@sveltejs/acorn-typescript": { - "version": "1.0.5", - "resolved": "https://registry.npmjs.org/@sveltejs/acorn-typescript/-/acorn-typescript-1.0.5.tgz", - "integrity": "sha512-IwQk4yfwLdibDlrXVE04jTZYlLnwsTT2PIOQQGNLWfjavGifnk1JD1LcZjZaBTRcxZu2FfPfNLOE04DSu9lqtQ==", + "version": "1.0.10", + "resolved": "https://registry.npmjs.org/@sveltejs/acorn-typescript/-/acorn-typescript-1.0.10.tgz", + "integrity": "sha512-4WfKk68eTih+MiJD4fSbxN7E8kVBmTMPWHUPYjvl2N0rMs53YLTT8/YjKU5Dtnz5LqDjl7LEw4U7lXR2W3J5WA==", "license": "MIT", "peerDependencies": { "acorn": "^8.9.0" @@ -1674,7 +1672,6 @@ "integrity": "sha512-ou/d51QSdTyN26D7h6dSpusAKaZkAiGM55/AKYi+9AGZw7q85hElbjK3kEyzXHhLSnRISHOYzVge6x0jRZ7DXA==", "dev": true, "license": "MIT", - "peer": true, "dependencies": { "@sveltejs/vite-plugin-svelte-inspector": "^5.0.0", "deepmerge": "^4.3.1", @@ -1732,12 +1729,12 @@ } }, "node_modules/@tanstack/svelte-virtual": { - "version": "3.13.23", - "resolved": "https://registry.npmjs.org/@tanstack/svelte-virtual/-/svelte-virtual-3.13.23.tgz", - "integrity": "sha512-kUFdKtevYPhuv9bN2/NhE8zCr6pO3lP2a4xYwyzwqvNZim0EMHdq9nKMFgS4rMHE9Iacdj4+PwMYDfRZ45a/+Q==", + "version": "3.13.28", + "resolved": "https://registry.npmjs.org/@tanstack/svelte-virtual/-/svelte-virtual-3.13.28.tgz", + "integrity": "sha512-8gI6SVgnWV6uxje8HyFtYK3xDO8UFxiEcKvWCliyz1QYsqM0wScA5vD6vvKJotPwKR4HKmrfLhb5OlDesbgqZw==", "license": "MIT", "dependencies": { - "@tanstack/virtual-core": "3.13.23" + "@tanstack/virtual-core": "3.17.0" }, "funding": { "type": "github", @@ -1748,9 +1745,9 @@ } }, "node_modules/@tanstack/virtual-core": { - "version": "3.13.23", - "resolved": "https://registry.npmjs.org/@tanstack/virtual-core/-/virtual-core-3.13.23.tgz", - "integrity": "sha512-zSz2Z2HNyLjCplANTDyl3BcdQJc2k1+yyFoKhNRmCr7V7dY8o8q5m8uFTI1/Pg1kL+Hgrz6u3Xo6eFUB7l66cg==", + "version": "3.17.0", + "resolved": "https://registry.npmjs.org/@tanstack/virtual-core/-/virtual-core-3.17.0.tgz", + "integrity": "sha512-gOxY/hFkPh/XQYhnThBHzkbkX3Ed+z/iushyz+R+JAr213aXxUDgQoTgTdrDpBSRsjFM73P/KfUyWmaF9WHMkQ==", "license": "MIT", "funding": { "type": "github", @@ -1800,13 +1797,6 @@ "dev": true, "license": "MIT" }, - "node_modules/@tootallnate/quickjs-emscripten": { - "version": "0.23.0", - "resolved": "https://registry.npmjs.org/@tootallnate/quickjs-emscripten/-/quickjs-emscripten-0.23.0.tgz", - "integrity": "sha512-C5Mc6rdnsaJDjO3UpGW/CQTHtCKaYlScZTly4JIu97Jxo/odCiH0ITnDXSJPTOrEKk/ycSZ0AOgTmkDtkOsvIA==", - "dev": true, - "license": "MIT" - }, "node_modules/@tsconfig/svelte": { "version": "5.0.8", "resolved": "https://registry.npmjs.org/@tsconfig/svelte/-/svelte-5.0.8.tgz", @@ -1896,20 +1886,20 @@ "license": "MIT" }, "node_modules/@typescript-eslint/eslint-plugin": { - "version": "8.57.1", - "resolved": "https://registry.npmjs.org/@typescript-eslint/eslint-plugin/-/eslint-plugin-8.57.1.tgz", - "integrity": "sha512-Gn3aqnvNl4NGc6x3/Bqk1AOn0thyTU9bqDRhiRnUWezgvr2OnhYCWCgC8zXXRVqBsIL1pSDt7T9nJUe0oM0kDQ==", + "version": "8.60.1", + "resolved": "https://registry.npmjs.org/@typescript-eslint/eslint-plugin/-/eslint-plugin-8.60.1.tgz", + "integrity": "sha512-JQ4S5GB0tfjO8BuJ4fcX+HodkzJjYBV+7OJ+wLygaX7OGQ7FudyHL4NSCA6ob+w3Yn+5MkKIozOwQhXeM7opVg==", "dev": true, "license": "MIT", "dependencies": { "@eslint-community/regexpp": "^4.12.2", - "@typescript-eslint/scope-manager": "8.57.1", - "@typescript-eslint/type-utils": "8.57.1", - "@typescript-eslint/utils": "8.57.1", - "@typescript-eslint/visitor-keys": "8.57.1", + "@typescript-eslint/scope-manager": "8.60.1", + "@typescript-eslint/type-utils": "8.60.1", + "@typescript-eslint/utils": "8.60.1", + "@typescript-eslint/visitor-keys": "8.60.1", "ignore": "^7.0.5", "natural-compare": "^1.4.0", - "ts-api-utils": "^2.4.0" + "ts-api-utils": "^2.5.0" }, "engines": { "node": "^18.18.0 || ^20.9.0 || >=21.1.0" @@ -1919,9 +1909,9 @@ "url": "https://opencollective.com/typescript-eslint" }, "peerDependencies": { - "@typescript-eslint/parser": "^8.57.1", + "@typescript-eslint/parser": "^8.60.1", "eslint": "^8.57.0 || ^9.0.0 || ^10.0.0", - "typescript": ">=4.8.4 <6.0.0" + "typescript": ">=4.8.4 <6.1.0" } }, "node_modules/@typescript-eslint/eslint-plugin/node_modules/ignore": { @@ -1935,17 +1925,16 @@ } }, "node_modules/@typescript-eslint/parser": { - "version": "8.57.1", - "resolved": "https://registry.npmjs.org/@typescript-eslint/parser/-/parser-8.57.1.tgz", - "integrity": "sha512-k4eNDan0EIMTT/dUKc/g+rsJ6wcHYhNPdY19VoX/EOtaAG8DLtKCykhrUnuHPYvinn5jhAPgD2Qw9hXBwrahsw==", + "version": "8.60.1", + "resolved": "https://registry.npmjs.org/@typescript-eslint/parser/-/parser-8.60.1.tgz", + "integrity": "sha512-A0M6ua6H252bVjPvvtSgl2QA4+ET9S5Mtkb2GDyTxIhH/C4qDItT7RQNO5PhMC6NXGYXOR9dIalcDDgBKT7oFA==", "dev": true, "license": "MIT", - "peer": true, "dependencies": { - "@typescript-eslint/scope-manager": "8.57.1", - "@typescript-eslint/types": "8.57.1", - "@typescript-eslint/typescript-estree": "8.57.1", - "@typescript-eslint/visitor-keys": "8.57.1", + "@typescript-eslint/scope-manager": "8.60.1", + "@typescript-eslint/types": "8.60.1", + "@typescript-eslint/typescript-estree": "8.60.1", + "@typescript-eslint/visitor-keys": "8.60.1", "debug": "^4.4.3" }, "engines": { @@ -1957,7 +1946,7 @@ }, "peerDependencies": { "eslint": "^8.57.0 || ^9.0.0 || ^10.0.0", - "typescript": ">=4.8.4 <6.0.0" + "typescript": ">=4.8.4 <6.1.0" } }, "node_modules/@typescript-eslint/parser/node_modules/debug": { @@ -1979,14 +1968,14 @@ } }, "node_modules/@typescript-eslint/project-service": { - "version": "8.57.1", - "resolved": "https://registry.npmjs.org/@typescript-eslint/project-service/-/project-service-8.57.1.tgz", - "integrity": "sha512-vx1F37BRO1OftsYlmG9xay1TqnjNVlqALymwWVuYTdo18XuKxtBpCj1QlzNIEHlvlB27osvXFWptYiEWsVdYsg==", + "version": "8.60.1", + "resolved": "https://registry.npmjs.org/@typescript-eslint/project-service/-/project-service-8.60.1.tgz", + "integrity": "sha512-eXkTH2bxmXlqD1RnOPmLZ9ZM9D3VwSx04JOwBnP9RQ+yUA5a2Mu7SfW8uaV2Aon53NJzZlZYuX7tn91Izf+xaw==", "dev": true, "license": "MIT", "dependencies": { - "@typescript-eslint/tsconfig-utils": "^8.57.1", - "@typescript-eslint/types": "^8.57.1", + "@typescript-eslint/tsconfig-utils": "^8.60.1", + "@typescript-eslint/types": "^8.60.1", "debug": "^4.4.3" }, "engines": { @@ -1997,7 +1986,7 @@ "url": "https://opencollective.com/typescript-eslint" }, "peerDependencies": { - "typescript": ">=4.8.4 <6.0.0" + "typescript": ">=4.8.4 <6.1.0" } }, "node_modules/@typescript-eslint/project-service/node_modules/debug": { @@ -2019,14 +2008,14 @@ } }, "node_modules/@typescript-eslint/scope-manager": { - "version": "8.57.1", - "resolved": "https://registry.npmjs.org/@typescript-eslint/scope-manager/-/scope-manager-8.57.1.tgz", - "integrity": "sha512-hs/QcpCwlwT2L5S+3fT6gp0PabyGk4Q0Rv2doJXA0435/OpnSR3VRgvrp8Xdoc3UAYSg9cyUjTeFXZEPg/3OKg==", + "version": "8.60.1", + "resolved": "https://registry.npmjs.org/@typescript-eslint/scope-manager/-/scope-manager-8.60.1.tgz", + "integrity": "sha512-gvI5OQoptnxQnchOirukCuQ55svJSTuD/4k5+pC267xyBtYry748R9/c3tYUzb/iE6RZfllRz2lVulLCHkTm4w==", "dev": true, "license": "MIT", "dependencies": { - "@typescript-eslint/types": "8.57.1", - "@typescript-eslint/visitor-keys": "8.57.1" + "@typescript-eslint/types": "8.60.1", + "@typescript-eslint/visitor-keys": "8.60.1" }, "engines": { "node": "^18.18.0 || ^20.9.0 || >=21.1.0" @@ -2037,9 +2026,9 @@ } }, "node_modules/@typescript-eslint/tsconfig-utils": { - "version": "8.57.1", - "resolved": "https://registry.npmjs.org/@typescript-eslint/tsconfig-utils/-/tsconfig-utils-8.57.1.tgz", - "integrity": "sha512-0lgOZB8cl19fHO4eI46YUx2EceQqhgkPSuCGLlGi79L2jwYY1cxeYc1Nae8Aw1xjgW3PKVDLlr3YJ6Bxx8HkWg==", + "version": "8.60.1", + "resolved": "https://registry.npmjs.org/@typescript-eslint/tsconfig-utils/-/tsconfig-utils-8.60.1.tgz", + "integrity": "sha512-nh8w4qAteiKuZu3pSSzG/yGKpw0OlkrKnzFmbVRenKaD4qc+7i1GrmZaLVkr8rk4uipiPGMOW4YsM6WmKZ5CvA==", "dev": true, "license": "MIT", "engines": { @@ -2050,21 +2039,21 @@ "url": "https://opencollective.com/typescript-eslint" }, "peerDependencies": { - "typescript": ">=4.8.4 <6.0.0" + "typescript": ">=4.8.4 <6.1.0" } }, "node_modules/@typescript-eslint/type-utils": { - "version": "8.57.1", - "resolved": "https://registry.npmjs.org/@typescript-eslint/type-utils/-/type-utils-8.57.1.tgz", - "integrity": "sha512-+Bwwm0ScukFdyoJsh2u6pp4S9ktegF98pYUU0hkphOOqdMB+1sNQhIz8y5E9+4pOioZijrkfNO/HUJVAFFfPKA==", + "version": "8.60.1", + "resolved": "https://registry.npmjs.org/@typescript-eslint/type-utils/-/type-utils-8.60.1.tgz", + "integrity": "sha512-sdwTrpjosW7ANQYJ39ZBF1ZyEMEGVB2UsikrserVM/30a/F1dTLnu9bGxEdosugyu5caigjLrR2qiD11asjI1A==", "dev": true, "license": "MIT", "dependencies": { - "@typescript-eslint/types": "8.57.1", - "@typescript-eslint/typescript-estree": "8.57.1", - "@typescript-eslint/utils": "8.57.1", + "@typescript-eslint/types": "8.60.1", + "@typescript-eslint/typescript-estree": "8.60.1", + "@typescript-eslint/utils": "8.60.1", "debug": "^4.4.3", - "ts-api-utils": "^2.4.0" + "ts-api-utils": "^2.5.0" }, "engines": { "node": "^18.18.0 || ^20.9.0 || >=21.1.0" @@ -2075,7 +2064,7 @@ }, "peerDependencies": { "eslint": "^8.57.0 || ^9.0.0 || ^10.0.0", - "typescript": ">=4.8.4 <6.0.0" + "typescript": ">=4.8.4 <6.1.0" } }, "node_modules/@typescript-eslint/type-utils/node_modules/debug": { @@ -2097,9 +2086,9 @@ } }, "node_modules/@typescript-eslint/types": { - "version": "8.57.1", - "resolved": "https://registry.npmjs.org/@typescript-eslint/types/-/types-8.57.1.tgz", - "integrity": "sha512-S29BOBPJSFUiblEl6RzPPjJt6w25A6XsBqRVDt53tA/tlL8q7ceQNZHTjPeONt/3S7KRI4quk+yP9jK2WjBiPQ==", + "version": "8.60.1", + "resolved": "https://registry.npmjs.org/@typescript-eslint/types/-/types-8.60.1.tgz", + "integrity": "sha512-4h0tY8ppCkdCzcrl2YM5M3my0xsE1Tf8om3owEu5oPWmXwkKRmk0j0LGDzYBGUcAlesEbxBhazqu/K4cu3Ug7w==", "devOptional": true, "license": "MIT", "engines": { @@ -2111,21 +2100,21 @@ } }, "node_modules/@typescript-eslint/typescript-estree": { - "version": "8.57.1", - "resolved": "https://registry.npmjs.org/@typescript-eslint/typescript-estree/-/typescript-estree-8.57.1.tgz", - "integrity": "sha512-ybe2hS9G6pXpqGtPli9Gx9quNV0TWLOmh58ADlmZe9DguLq0tiAKVjirSbtM1szG6+QH6rVXyU6GTLQbWnMY+g==", + "version": "8.60.1", + "resolved": "https://registry.npmjs.org/@typescript-eslint/typescript-estree/-/typescript-estree-8.60.1.tgz", + "integrity": "sha512-alpRkfG8hlVE5kdJW2GkfgDgXxold3e8e4l6EnmhRmRLbekgAPCCGDVD++sABy9FcgPFroq+uFcCSM1vR57Cew==", "dev": true, "license": "MIT", "dependencies": { - "@typescript-eslint/project-service": "8.57.1", - "@typescript-eslint/tsconfig-utils": "8.57.1", - "@typescript-eslint/types": "8.57.1", - "@typescript-eslint/visitor-keys": "8.57.1", + "@typescript-eslint/project-service": "8.60.1", + "@typescript-eslint/tsconfig-utils": "8.60.1", + "@typescript-eslint/types": "8.60.1", + "@typescript-eslint/visitor-keys": "8.60.1", "debug": "^4.4.3", "minimatch": "^10.2.2", "semver": "^7.7.3", "tinyglobby": "^0.2.15", - "ts-api-utils": "^2.4.0" + "ts-api-utils": "^2.5.0" }, "engines": { "node": "^18.18.0 || ^20.9.0 || >=21.1.0" @@ -2135,7 +2124,7 @@ "url": "https://opencollective.com/typescript-eslint" }, "peerDependencies": { - "typescript": ">=4.8.4 <6.0.0" + "typescript": ">=4.8.4 <6.1.0" } }, "node_modules/@typescript-eslint/typescript-estree/node_modules/debug": { @@ -2157,16 +2146,16 @@ } }, "node_modules/@typescript-eslint/utils": { - "version": "8.57.1", - "resolved": "https://registry.npmjs.org/@typescript-eslint/utils/-/utils-8.57.1.tgz", - "integrity": "sha512-XUNSJ/lEVFttPMMoDVA2r2bwrl8/oPx8cURtczkSEswY5T3AeLmCy+EKWQNdL4u0MmAHOjcWrqJp2cdvgjn8dQ==", + "version": "8.60.1", + "resolved": "https://registry.npmjs.org/@typescript-eslint/utils/-/utils-8.60.1.tgz", + "integrity": "sha512-h2MPBLoNtjc3qZWfY3Tl51yPorQ2McHn8pJfcMNTcIvrrZrr90Ykffit0yjrPFWQcRcUxzH20+6OcVdW4yHtUg==", "dev": true, "license": "MIT", "dependencies": { "@eslint-community/eslint-utils": "^4.9.1", - "@typescript-eslint/scope-manager": "8.57.1", - "@typescript-eslint/types": "8.57.1", - "@typescript-eslint/typescript-estree": "8.57.1" + "@typescript-eslint/scope-manager": "8.60.1", + "@typescript-eslint/types": "8.60.1", + "@typescript-eslint/typescript-estree": "8.60.1" }, "engines": { "node": "^18.18.0 || ^20.9.0 || >=21.1.0" @@ -2177,17 +2166,17 @@ }, "peerDependencies": { "eslint": "^8.57.0 || ^9.0.0 || ^10.0.0", - "typescript": ">=4.8.4 <6.0.0" + "typescript": ">=4.8.4 <6.1.0" } }, "node_modules/@typescript-eslint/visitor-keys": { - "version": "8.57.1", - "resolved": "https://registry.npmjs.org/@typescript-eslint/visitor-keys/-/visitor-keys-8.57.1.tgz", - "integrity": "sha512-YWnmJkXbofiz9KbnbbwuA2rpGkFPLbAIetcCNO6mJ8gdhdZ/v7WDXsoGFAJuM6ikUFKTlSQnjWnVO4ux+UzS6A==", + "version": "8.60.1", + "resolved": "https://registry.npmjs.org/@typescript-eslint/visitor-keys/-/visitor-keys-8.60.1.tgz", + "integrity": "sha512-EbGRQg4FhrmwLodl+t3JNAnXHWVr9Vp+Zl1QBZVPY4ByfkzIT8cX3K6QWODHtkIZqqJVEWvhHSx3v5PDHsaQag==", "dev": true, "license": "MIT", "dependencies": { - "@typescript-eslint/types": "8.57.1", + "@typescript-eslint/types": "8.60.1", "eslint-visitor-keys": "^5.0.0" }, "engines": { @@ -2515,7 +2504,6 @@ "resolved": "https://registry.npmjs.org/acorn/-/acorn-8.15.0.tgz", "integrity": "sha512-NZyJarBfL7nWwIq+FDL6Zp/yHEhePMNnnJ0y3qfieCrmNvYct8uvtiV41UvlSe6apAfk0fY1FbWx+NwfmpvtTg==", "license": "MIT", - "peer": true, "bin": { "acorn": "bin/acorn" }, @@ -2534,13 +2522,16 @@ } }, "node_modules/agent-base": { - "version": "7.1.3", - "resolved": "https://registry.npmjs.org/agent-base/-/agent-base-7.1.3.tgz", - "integrity": "sha512-jRR5wdylq8CkOe6hei19GGZnxM6rBGwFl3Bg0YItGDimvjGtAvdZk4Pu6Cl4u4Igsws4a1fd1Vq3ezrhn4KmFw==", + "version": "6.0.2", + "resolved": "https://registry.npmjs.org/agent-base/-/agent-base-6.0.2.tgz", + "integrity": "sha512-RZNwNclF7+MS/8bDg70amg32dyeZGZxiDuQmZxKLAlQjr3jGyLx+4Kkk58UO7D2QdgFIQCovuSuZESne6RG6XQ==", "dev": true, "license": "MIT", + "dependencies": { + "debug": "4" + }, "engines": { - "node": ">= 14" + "node": ">= 6.0.0" } }, "node_modules/ansi-escapes": { @@ -2797,28 +2788,18 @@ } }, "node_modules/axios": { - "version": "1.16.0", - "resolved": "https://registry.npmjs.org/axios/-/axios-1.16.0.tgz", - "integrity": "sha512-6hp5CwvTPlN2A31g5dxnwAX0orzM7pmCRDLnZSX772mv8WDqICwFjowHuPs04Mc8deIld1+ejhtaMn5vp6b+1w==", + "version": "1.17.0", + "resolved": "https://registry.npmjs.org/axios/-/axios-1.17.0.tgz", + "integrity": "sha512-J8SwNxprqqpbfenehxWYXE7CW+wM1BB4w3+N+g+/Wx40xM4rsLrfPmHHxSWIxJLYDgSY/HqlFPIYb2/S3rxafw==", "dev": true, "license": "MIT", - "peer": true, "dependencies": { "follow-redirects": "^1.16.0", "form-data": "^4.0.5", + "https-proxy-agent": "^5.0.1", "proxy-from-env": "^2.1.0" } }, - "node_modules/axios/node_modules/proxy-from-env": { - "version": "2.1.0", - "resolved": "https://registry.npmjs.org/proxy-from-env/-/proxy-from-env-2.1.0.tgz", - "integrity": "sha512-cJ+oHTW1VAEa8cJslgmUZrc+sjRKgAKl3Zyse6+PV38hZe/V6Z14TbCuXcan9F9ghlz4QrFr2c92TNF82UkYHA==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=10" - } - }, "node_modules/axobject-query": { "version": "4.1.0", "resolved": "https://registry.npmjs.org/axobject-query/-/axobject-query-4.1.0.tgz", @@ -2853,9 +2834,9 @@ } }, "node_modules/basic-ftp": { - "version": "5.2.2", - "resolved": "https://registry.npmjs.org/basic-ftp/-/basic-ftp-5.2.2.tgz", - "integrity": "sha512-1tDrzKsdCg70WGvbFss/ulVAxupNauGnOlgpyjKzeQxzyllBLS0CGLV7tjIXTK3ZQA9/FBEm9qyFFN1bciA6pw==", + "version": "5.3.1", + "resolved": "https://registry.npmjs.org/basic-ftp/-/basic-ftp-5.3.1.tgz", + "integrity": "sha512-bopVNp6ugyA150DDuZfPFdt1KZ5a94ZDiwX4hMgZDzF+GttD80lEy8kj98kbyhLXnPvhtIo93mdnLIjpCAeeOw==", "dev": true, "license": "MIT", "engines": { @@ -3305,13 +3286,13 @@ } }, "node_modules/data-uri-to-buffer": { - "version": "6.0.2", - "resolved": "https://registry.npmjs.org/data-uri-to-buffer/-/data-uri-to-buffer-6.0.2.tgz", - "integrity": "sha512-7hvf7/GW8e86rW0ptuwS3OcBGDjIi6SZva7hCyWC0yYry2cOPmLIjXAUHI6DK2HsnwJd9ifmt57i8eV2n4YNpw==", + "version": "8.0.0", + "resolved": "https://registry.npmjs.org/data-uri-to-buffer/-/data-uri-to-buffer-8.0.0.tgz", + "integrity": "sha512-6UHfyCux51b8PTGDgveqtz1tvphBku5DrMKKJbFAZAJOI2zsjDpDoYE1+QGj7FOMS4BdTFNJsJiR3zEB0xH0yQ==", "dev": true, "license": "MIT", "engines": { - "node": ">= 14" + "node": ">= 20" } }, "node_modules/data-view-buffer": { @@ -3369,9 +3350,9 @@ } }, "node_modules/date-fns": { - "version": "4.1.0", - "resolved": "https://registry.npmjs.org/date-fns/-/date-fns-4.1.0.tgz", - "integrity": "sha512-Ukq0owbQXxa/U3EGtsdVBkR1w7KOQ5gIBqdH2hkvknzZPYvBxb/aa6E8L7tmjFtkwZBu3UXBbjIgPo/Ez4xaNg==", + "version": "4.4.0", + "resolved": "https://registry.npmjs.org/date-fns/-/date-fns-4.4.0.tgz", + "integrity": "sha512-+1UMbeh68lH1SegH83CGWwpb6OHHbpSgr3+s5Eww5M4CAgswBpoWS0AjTOfEJ33HiYKz1hdj/KTFprzXHmq/6w==", "dev": true, "license": "MIT", "funding": { @@ -3474,9 +3455,9 @@ } }, "node_modules/degenerator": { - "version": "5.0.1", - "resolved": "https://registry.npmjs.org/degenerator/-/degenerator-5.0.1.tgz", - "integrity": "sha512-TllpMR/t0M5sqCXfj85i4XaAzxmS5tVA16dqvdkMwGmzI+dXLXnw3J+3Vdv7VKw+ThlTMboK6i9rnZ6Nntj5CQ==", + "version": "7.0.1", + "resolved": "https://registry.npmjs.org/degenerator/-/degenerator-7.0.1.tgz", + "integrity": "sha512-ABErK0IefDSyHjlPH7WUEenIAX2rPPnrDcDM+TS3z3+zu9TfyKKi07BQM+8rmxpdE2y1v5fjjdoAS/x4D2U60w==", "dev": true, "license": "MIT", "dependencies": { @@ -3485,7 +3466,10 @@ "esprima": "^4.0.1" }, "engines": { - "node": ">= 14" + "node": ">= 20" + }, + "peerDependencies": { + "quickjs-wasi": "^2.2.0" } }, "node_modules/delayed-stream": { @@ -3819,7 +3803,6 @@ "integrity": "sha512-LEyamqS7W5HB3ujJyvi0HQK/dtVINZvd5mAAp9eT5S/ujByGjiZLCzPcHVzuXbpJDJF/cxwHlfceVUDZ2lnSTw==", "dev": true, "license": "MIT", - "peer": true, "dependencies": { "@eslint-community/eslint-utils": "^4.8.0", "@eslint-community/regexpp": "^4.12.1", @@ -3921,10 +3904,11 @@ } }, "node_modules/eslint-import-resolver-typescript": { - "version": "4.4.4", - "resolved": "https://registry.npmjs.org/eslint-import-resolver-typescript/-/eslint-import-resolver-typescript-4.4.4.tgz", - "integrity": "sha512-1iM2zeBvrYmUNTj2vSC/90JTHDth+dfOfiNKkxApWRsTJYNrc8rOdxxIf5vazX+BiAXTeOT0UvWpGI/7qIWQOw==", + "version": "4.4.5", + "resolved": "https://registry.npmjs.org/eslint-import-resolver-typescript/-/eslint-import-resolver-typescript-4.4.5.tgz", + "integrity": "sha512-nbE5XLph6TLtGYcu/U6e6ZVXyKBhbDWK5cLGk76eJ7NdZpwf1P9EFkpt1Z01mNZNrrilsAYWKH6zUkL4reoXbw==", "dev": true, + "license": "ISC", "dependencies": { "debug": "^4.4.1", "eslint-import-context": "^0.1.8", @@ -4008,7 +3992,6 @@ "integrity": "sha512-whOE1HFo/qJDyX4SnXzP4N6zOWn79WhnCUY/iDR0mPfQZO8wcYE4JClzI2oZrhBnnMUCBCHZhO6VQyoBU95mZA==", "dev": true, "license": "MIT", - "peer": true, "dependencies": { "@rtsao/scc": "^1.1.0", "array-includes": "^3.1.9", @@ -4153,9 +4136,9 @@ } }, "node_modules/eslint-plugin-svelte": { - "version": "3.15.2", - "resolved": "https://registry.npmjs.org/eslint-plugin-svelte/-/eslint-plugin-svelte-3.15.2.tgz", - "integrity": "sha512-k4Nsjs3bHujeEnnckoTM4mFYR1e8Mb9l2rTwNdmYiamA+Tjzn8X+2F+fuSP2w4VbXYhn2bmySyACQYdmUDW2Cg==", + "version": "3.19.0", + "resolved": "https://registry.npmjs.org/eslint-plugin-svelte/-/eslint-plugin-svelte-3.19.0.tgz", + "integrity": "sha512-t3rNaZeXz4d2gG4uJyMEYfJCFKf22+SWbSizIIXIWKu4wM+XPLiMWuSSr/C5821JmFeN9ogK+eExbG+z+twyxw==", "dev": true, "license": "MIT", "dependencies": { @@ -4168,7 +4151,7 @@ "postcss-load-config": "^3.1.4", "postcss-safe-parser": "^7.0.0", "semver": "^7.6.3", - "svelte-eslint-parser": "^1.4.0" + "svelte-eslint-parser": "^1.7.0" }, "engines": { "node": "^18.18.0 || ^20.9.0 || >=21.1.0" @@ -4355,9 +4338,9 @@ } }, "node_modules/esrap": { - "version": "2.2.8", - "resolved": "https://registry.npmjs.org/esrap/-/esrap-2.2.8.tgz", - "integrity": "sha512-MPweq2EvEGj8jwOI7Hgycw/QIHzqA1EbAM8lG7p+FBfZbZq/hQ6h3AMsqnu/djzisH1KVWNzbb7LSgIVtMlPSg==", + "version": "2.2.11", + "resolved": "https://registry.npmjs.org/esrap/-/esrap-2.2.11.tgz", + "integrity": "sha512-gPdx+I+BjYEinNMQaBXFjbaJVyoPMU4ZODg5mE+M4DqVG9VusAVHHjcBX+zqyITlI0DIARwDMMzZwAWj36dRoQ==", "license": "MIT", "dependencies": { "@jridgewell/sourcemap-codec": "^1.4.15" @@ -4490,9 +4473,9 @@ } }, "node_modules/file-type": { - "version": "21.3.2", - "resolved": "https://registry.npmjs.org/file-type/-/file-type-21.3.2.tgz", - "integrity": "sha512-DLkUvGwep3poOV2wpzbHCOnSKGk1LzyXTv+aHFgN2VFl96wnp8YA9YjO2qPzg5PuL8q/SW9Pdi6WTkYOIh995w==", + "version": "21.3.4", + "resolved": "https://registry.npmjs.org/file-type/-/file-type-21.3.4.tgz", + "integrity": "sha512-Ievi/yy8DS3ygGvT47PjSfdFoX+2isQueoYP1cntFW1JLYAuS4GD7NUPGg4zv2iZfV52uDyk5w5Z0TdpRS6Q1g==", "dev": true, "license": "MIT", "dependencies": { @@ -4621,9 +4604,9 @@ "license": "ISC" }, "node_modules/fs-extra": { - "version": "11.3.4", - "resolved": "https://registry.npmjs.org/fs-extra/-/fs-extra-11.3.4.tgz", - "integrity": "sha512-CTXd6rk/M3/ULNQj8FBqBWHYBVYybQ3VPBw0xGKFe3tuH7ytT6ACnvzpIQ3UZtB8yvUKC2cXn1a+x+5EVQLovA==", + "version": "11.3.5", + "resolved": "https://registry.npmjs.org/fs-extra/-/fs-extra-11.3.5.tgz", + "integrity": "sha512-eKpRKAovdpZtR1WopLHxlBWvAgPny3c4gX1G5Jhwmmw4XJj0ifSD5qB5TOo8hmA0wlRKDAOAhEE1yVPgs6Fgcg==", "dev": true, "license": "MIT", "dependencies": { @@ -4771,18 +4754,18 @@ } }, "node_modules/get-uri": { - "version": "6.0.4", - "resolved": "https://registry.npmjs.org/get-uri/-/get-uri-6.0.4.tgz", - "integrity": "sha512-E1b1lFFLvLgak2whF2xDBcOy6NLVGZBqqjJjsIhvopKfWWEi64pLVTWWehV8KlLerZkfNTA95sTe2OdJKm1OzQ==", + "version": "8.0.0", + "resolved": "https://registry.npmjs.org/get-uri/-/get-uri-8.0.0.tgz", + "integrity": "sha512-CqtZlMKvfJeY0Zxv8wazDwXmSKmnMnsmNy8j8+wudi8EyG/pMUB1NqHc+Tv1QaNtpYsK9nOYjb7r7Ufu32RPSw==", "dev": true, "license": "MIT", "dependencies": { - "basic-ftp": "^5.0.2", - "data-uri-to-buffer": "^6.0.2", + "basic-ftp": "^5.2.0", + "data-uri-to-buffer": "8.0.0", "debug": "^4.3.4" }, "engines": { - "node": ">= 14" + "node": ">= 20" } }, "node_modules/glob": { @@ -4968,31 +4951,41 @@ } }, "node_modules/http-proxy-agent": { - "version": "7.0.2", - "resolved": "https://registry.npmjs.org/http-proxy-agent/-/http-proxy-agent-7.0.2.tgz", - "integrity": "sha512-T1gkAiYYDWYx3V5Bmyu7HcfcvL7mUrTWiM6yOfa3PIphViJ/gFPbvidQ+veqSOHci/PxBcDabeUNCzpOODJZig==", + "version": "9.0.0", + "resolved": "https://registry.npmjs.org/http-proxy-agent/-/http-proxy-agent-9.0.0.tgz", + "integrity": "sha512-FcF8VhXYLQcxWCnt/cCpT2apKsRDUGeVEeMqGu4HSTu29U8Yw0TLOjdYIlDsYk3IkUh+taX4IDWpPcCqKDhCjA==", "dev": true, "license": "MIT", "dependencies": { - "agent-base": "^7.1.0", + "agent-base": "9.0.0", "debug": "^4.3.4" }, "engines": { - "node": ">= 14" + "node": ">= 20" + } + }, + "node_modules/http-proxy-agent/node_modules/agent-base": { + "version": "9.0.0", + "resolved": "https://registry.npmjs.org/agent-base/-/agent-base-9.0.0.tgz", + "integrity": "sha512-TQf59BsZnytt8GdJKLPfUZ54g/iaUL2OWDSFCCvMOhsHduDQxO8xC4PNeyIkVcA5KwL2phPSv0douC0fgWzmnA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 20" } }, "node_modules/https-proxy-agent": { - "version": "7.0.6", - "resolved": "https://registry.npmjs.org/https-proxy-agent/-/https-proxy-agent-7.0.6.tgz", - "integrity": "sha512-vK9P5/iUfdl95AI+JVyUuIcVtd4ofvtrOr3HNtM2yxC9bnMbEdp3x01OhQNnjb8IJYi38VlTE3mBXwcfvywuSw==", + "version": "5.0.1", + "resolved": "https://registry.npmjs.org/https-proxy-agent/-/https-proxy-agent-5.0.1.tgz", + "integrity": "sha512-dFcAjpTQFgoLMzC2VwU+C/CbS7uRL0lWmxDITmqm7C+7F0Odmj6s9l6alZc6AELXhrnggM2CeWSXHGOdX2YtwA==", "dev": true, "license": "MIT", "dependencies": { - "agent-base": "^7.1.2", + "agent-base": "6", "debug": "4" }, "engines": { - "node": ">= 14" + "node": ">= 6" } }, "node_modules/ieee754": { @@ -5603,9 +5596,9 @@ } }, "node_modules/jsonfile": { - "version": "6.2.0", - "resolved": "https://registry.npmjs.org/jsonfile/-/jsonfile-6.2.0.tgz", - "integrity": "sha512-FGuPw30AdOIUTRMC2OMRtQV+jkVj2cfPqSeWXv1NEAJ1qZ5zb1X6z1mFhbfOB/iy3ssJCD+3KuZ8r8C3uVFlAg==", + "version": "6.2.1", + "resolved": "https://registry.npmjs.org/jsonfile/-/jsonfile-6.2.1.tgz", + "integrity": "sha512-zwOTdL3rFQ/lRdBnntKVOX6k5cKJwEc1HdilT71BWEu7J41gXIB2MRp+vxduPSwZJPWBxEzv4yH1wYLJGUHX4Q==", "dev": true, "license": "MIT", "dependencies": { @@ -5871,9 +5864,9 @@ } }, "node_modules/netmask": { - "version": "2.0.2", - "resolved": "https://registry.npmjs.org/netmask/-/netmask-2.0.2.tgz", - "integrity": "sha512-dBpDMdxv9Irdq66304OLfEmQ9tbNRFnFTuZiLo+bD+r332bBmMJ8GBLXklIXXgxd3+v9+KUnZaUR5PJMa75Gsg==", + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/netmask/-/netmask-2.1.1.tgz", + "integrity": "sha512-eonl3sLUha+S1GzTPxychyhnUzKyeQkZ7jLjKrBagJgPla13F+uQ71HgpFefyHgqrjEbCPkDArxYsjY8/+gLKA==", "dev": true, "license": "MIT", "engines": { @@ -6038,13 +6031,13 @@ } }, "node_modules/otpauth": { - "version": "9.5.0", - "resolved": "https://registry.npmjs.org/otpauth/-/otpauth-9.5.0.tgz", - "integrity": "sha512-Ldhc6UYl4baR5toGr8nfKC+L/b8/RgHKoIixAebgoNGzUUCET02g04rMEZ2ZsPfeVQhMHcuaOgb28nwMr81zCA==", + "version": "9.5.1", + "resolved": "https://registry.npmjs.org/otpauth/-/otpauth-9.5.1.tgz", + "integrity": "sha512-fJmDAHc8wImfqqqOXIlBvT1dEKrZK0Cmb2VEgScpNTolCz0PHh6ExUZGv4sLtOsWNaHCQlD+rRqaPgnoxFoZjQ==", "dev": true, "license": "MIT", "dependencies": { - "@noble/hashes": "2.0.1" + "@noble/hashes": "2.2.0" }, "funding": { "url": "https://github.com/hectorm/otpauth?sponsor=1" @@ -6111,37 +6104,64 @@ } }, "node_modules/pac-proxy-agent": { - "version": "7.2.0", - "resolved": "https://registry.npmjs.org/pac-proxy-agent/-/pac-proxy-agent-7.2.0.tgz", - "integrity": "sha512-TEB8ESquiLMc0lV8vcd5Ql/JAKAoyzHFXaStwjkzpOpC5Yv+pIzLfHvjTSdf3vpa2bMiUQrg9i6276yn8666aA==", + "version": "9.0.1", + "resolved": "https://registry.npmjs.org/pac-proxy-agent/-/pac-proxy-agent-9.0.1.tgz", + "integrity": "sha512-3ZOSpLboOlpW4yp8Cuv21KlTULRqyJ5Uuad3wXpSKFrxdNgcHEyoa22GRaZ2UlgCVuR6z+5BiavtYVvbajL/Yw==", "dev": true, "license": "MIT", "dependencies": { - "@tootallnate/quickjs-emscripten": "^0.23.0", - "agent-base": "^7.1.2", + "agent-base": "9.0.0", "debug": "^4.3.4", - "get-uri": "^6.0.1", - "http-proxy-agent": "^7.0.0", - "https-proxy-agent": "^7.0.6", - "pac-resolver": "^7.0.1", - "socks-proxy-agent": "^8.0.5" + "get-uri": "8.0.0", + "http-proxy-agent": "9.0.0", + "https-proxy-agent": "9.0.0", + "pac-resolver": "9.0.1", + "quickjs-wasi": "^2.2.0", + "socks-proxy-agent": "10.0.0" }, "engines": { - "node": ">= 14" + "node": ">= 20" + } + }, + "node_modules/pac-proxy-agent/node_modules/agent-base": { + "version": "9.0.0", + "resolved": "https://registry.npmjs.org/agent-base/-/agent-base-9.0.0.tgz", + "integrity": "sha512-TQf59BsZnytt8GdJKLPfUZ54g/iaUL2OWDSFCCvMOhsHduDQxO8xC4PNeyIkVcA5KwL2phPSv0douC0fgWzmnA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 20" + } + }, + "node_modules/pac-proxy-agent/node_modules/https-proxy-agent": { + "version": "9.0.0", + "resolved": "https://registry.npmjs.org/https-proxy-agent/-/https-proxy-agent-9.0.0.tgz", + "integrity": "sha512-/MVmHp58WkOypgFhCLk4fzpPcFQvTJ/e6LBI7irpIO2HfxUbpmYoHF+KzipzJpxxzJu7aJNWQ0xojJ/dzV2G5g==", + "dev": true, + "license": "MIT", + "dependencies": { + "agent-base": "9.0.0", + "debug": "^4.3.4" + }, + "engines": { + "node": ">= 20" } }, "node_modules/pac-resolver": { - "version": "7.0.1", - "resolved": "https://registry.npmjs.org/pac-resolver/-/pac-resolver-7.0.1.tgz", - "integrity": "sha512-5NPgf87AT2STgwa2ntRMr45jTKrYBGkVU36yT0ig/n/GMAa3oPqhZfIQ2kMEimReg0+t9kZViDVZ83qfVUlckg==", + "version": "9.0.1", + "resolved": "https://registry.npmjs.org/pac-resolver/-/pac-resolver-9.0.1.tgz", + "integrity": "sha512-lJbS008tmkj08VhoM8Hzuv/VE5tK9MS0OIQ/7+s0lIF+BYhiQWFYzkSpML7lXs9iBu2jfmzBTLzhe9n6BX+dYw==", "dev": true, "license": "MIT", "dependencies": { - "degenerator": "^5.0.0", + "degenerator": "7.0.1", "netmask": "^2.0.2" }, "engines": { - "node": ">= 14" + "node": ">= 20" + }, + "peerDependencies": { + "quickjs-wasi": "^2.2.0" } }, "node_modules/parent-module": { @@ -6272,7 +6292,6 @@ } ], "license": "MIT", - "peer": true, "dependencies": { "nanoid": "^3.3.11", "picocolors": "^1.1.1", @@ -6377,9 +6396,9 @@ } }, "node_modules/postcss-selector-parser": { - "version": "7.1.0", - "resolved": "https://registry.npmjs.org/postcss-selector-parser/-/postcss-selector-parser-7.1.0.tgz", - "integrity": "sha512-8sLjZwK0R+JlxlYcTuVnyT2v+htpdrjDOKuMcOVdYjt52Lh8hWRYpxBPoKx/Zg+bcjc3wx6fmQevMmUztS/ccA==", + "version": "7.1.2", + "resolved": "https://registry.npmjs.org/postcss-selector-parser/-/postcss-selector-parser-7.1.2.tgz", + "integrity": "sha512-Wjvt4scRFouioIInHf51IFNP4ltJ2EngJM+cZPGiqbKetBfmP3vpdPV8ID2S6JS6/jdo74N8+aEYH9lQr2C6sA==", "dev": true, "license": "MIT", "dependencies": { @@ -6401,23 +6420,47 @@ } }, "node_modules/proxy-agent": { - "version": "6.5.0", - "resolved": "https://registry.npmjs.org/proxy-agent/-/proxy-agent-6.5.0.tgz", - "integrity": "sha512-TmatMXdr2KlRiA2CyDu8GqR8EjahTG3aY3nXjdzFyoZbmB8hrBsTyMezhULIXKnC0jpfjlmiZ3+EaCzoInSu/A==", + "version": "8.0.1", + "resolved": "https://registry.npmjs.org/proxy-agent/-/proxy-agent-8.0.1.tgz", + "integrity": "sha512-kccqGBqHZXR8onQhY/ganJjoO8QIKKRiFBhPOzbTZK16attzSZ/0XSmp9H7jrRxPKHjhGyx1q32lMPrJ3uLFgA==", "dev": true, "license": "MIT", "dependencies": { - "agent-base": "^7.1.2", + "agent-base": "9.0.0", "debug": "^4.3.4", - "http-proxy-agent": "^7.0.1", - "https-proxy-agent": "^7.0.6", + "http-proxy-agent": "9.0.0", + "https-proxy-agent": "9.0.0", "lru-cache": "^7.14.1", - "pac-proxy-agent": "^7.1.0", - "proxy-from-env": "^1.1.0", - "socks-proxy-agent": "^8.0.5" + "pac-proxy-agent": "9.0.1", + "proxy-from-env": "^2.0.0", + "socks-proxy-agent": "10.0.0" + }, + "engines": { + "node": ">= 20" + } + }, + "node_modules/proxy-agent/node_modules/agent-base": { + "version": "9.0.0", + "resolved": "https://registry.npmjs.org/agent-base/-/agent-base-9.0.0.tgz", + "integrity": "sha512-TQf59BsZnytt8GdJKLPfUZ54g/iaUL2OWDSFCCvMOhsHduDQxO8xC4PNeyIkVcA5KwL2phPSv0douC0fgWzmnA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 20" + } + }, + "node_modules/proxy-agent/node_modules/https-proxy-agent": { + "version": "9.0.0", + "resolved": "https://registry.npmjs.org/https-proxy-agent/-/https-proxy-agent-9.0.0.tgz", + "integrity": "sha512-/MVmHp58WkOypgFhCLk4fzpPcFQvTJ/e6LBI7irpIO2HfxUbpmYoHF+KzipzJpxxzJu7aJNWQ0xojJ/dzV2G5g==", + "dev": true, + "license": "MIT", + "dependencies": { + "agent-base": "9.0.0", + "debug": "^4.3.4" }, "engines": { - "node": ">= 14" + "node": ">= 20" } }, "node_modules/proxy-agent/node_modules/lru-cache": { @@ -6431,11 +6474,14 @@ } }, "node_modules/proxy-from-env": { - "version": "1.1.0", - "resolved": "https://registry.npmjs.org/proxy-from-env/-/proxy-from-env-1.1.0.tgz", - "integrity": "sha512-D+zkORCbA9f1tdWRK0RaCR3GPv50cMxcrz4X8k5LTSUD1Dkw47mKJEZQNunItRTkWwgtaUSo1RVFRIG9ZXiFYg==", + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/proxy-from-env/-/proxy-from-env-2.1.0.tgz", + "integrity": "sha512-cJ+oHTW1VAEa8cJslgmUZrc+sjRKgAKl3Zyse6+PV38hZe/V6Z14TbCuXcan9F9ghlz4QrFr2c92TNF82UkYHA==", "dev": true, - "license": "MIT" + "license": "MIT", + "engines": { + "node": ">=10" + } }, "node_modules/punycode": { "version": "2.3.1", @@ -6577,6 +6623,13 @@ "node": ">=6" } }, + "node_modules/quickjs-wasi": { + "version": "2.2.0", + "resolved": "https://registry.npmjs.org/quickjs-wasi/-/quickjs-wasi-2.2.0.tgz", + "integrity": "sha512-zQxXmQMrEoD3S+jQdYsloq4qAuaxKFHZj6hHqOYGwB2iQZH+q9e/lf5zQPXCKOk0WJuAjzRFbO4KwHIp2D05Iw==", + "dev": true, + "license": "MIT" + }, "node_modules/readdirp": { "version": "3.6.0", "resolved": "https://registry.npmjs.org/readdirp/-/readdirp-3.6.0.tgz", @@ -6608,8 +6661,7 @@ "resolved": "https://registry.npmjs.org/reflect-metadata/-/reflect-metadata-0.2.2.tgz", "integrity": "sha512-urBwgfrvVP/eAyXx4hluJivBKzuEbSQs9rKWCrCkbSxNv8mxPcUZKeuoF3Uy4mJl3Lwprp6yy5/39VWigZ4K6Q==", "dev": true, - "license": "Apache-2.0", - "peer": true + "license": "Apache-2.0" }, "node_modules/reflect.getprototypeof": { "version": "1.0.10", @@ -6797,7 +6849,6 @@ "integrity": "sha512-dhKf903U/PQZY6boNNtAGdWbG85WAbjT/1xYoZIC7FAY0yWapOBQVsVrDl58W86//e1VpMNBtRV4MaXfdMySFA==", "dev": true, "license": "Apache-2.0", - "peer": true, "dependencies": { "tslib": "^2.1.0" } @@ -6876,7 +6927,6 @@ "integrity": "sha512-AaIqGSrjo5lA2Yg7RvFZrlXDBCp3nV4XP73GrLGvdRWWwk+8H3l0SDvq/5bA4eF+0RFPLuWUk3E+P1U/YqnpsQ==", "dev": true, "license": "MIT", - "peer": true, "dependencies": { "chokidar": ">=3.0.0 <4.0.0", "immutable": "^4.0.0", @@ -7095,9 +7145,9 @@ } }, "node_modules/socks": { - "version": "2.8.8", - "resolved": "https://registry.npmjs.org/socks/-/socks-2.8.8.tgz", - "integrity": "sha512-NlGELfPrgX2f1TAAcz0WawlLn+0r3FyhhCRpFFK2CemXenPYvzMWWZINv3eDNo9ucdwme7oCHRY0Jnbs4aIkog==", + "version": "2.8.9", + "resolved": "https://registry.npmjs.org/socks/-/socks-2.8.9.tgz", + "integrity": "sha512-LJhUYUvItdQ0LkJTmPeaEObWXAqFyfmP85x0tch/ez9cahmhlBBLbIqDFnvBnUJGagb0JbIQrkBs1wJ+yRYpEw==", "dev": true, "license": "MIT", "dependencies": { @@ -7110,18 +7160,28 @@ } }, "node_modules/socks-proxy-agent": { - "version": "8.0.5", - "resolved": "https://registry.npmjs.org/socks-proxy-agent/-/socks-proxy-agent-8.0.5.tgz", - "integrity": "sha512-HehCEsotFqbPW9sJ8WVYB6UbmIMv7kUUORIF2Nncq4VQvBfNBLibW9YZR5dlYCSUhwcD628pRllm7n+E+YTzJw==", + "version": "10.0.0", + "resolved": "https://registry.npmjs.org/socks-proxy-agent/-/socks-proxy-agent-10.0.0.tgz", + "integrity": "sha512-pyp2YR3mNxAMu0mGLtzs4g7O3uT4/9sQOLAKcViAkaS9fJWkud7nmaf6ZREFqQEi24IPkBcjfHjXhPTUWjo3uA==", "dev": true, "license": "MIT", "dependencies": { - "agent-base": "^7.1.2", + "agent-base": "9.0.0", "debug": "^4.3.4", "socks": "^2.8.3" }, "engines": { - "node": ">= 14" + "node": ">= 20" + } + }, + "node_modules/socks-proxy-agent/node_modules/agent-base": { + "version": "9.0.0", + "resolved": "https://registry.npmjs.org/agent-base/-/agent-base-9.0.0.tgz", + "integrity": "sha512-TQf59BsZnytt8GdJKLPfUZ54g/iaUL2OWDSFCCvMOhsHduDQxO8xC4PNeyIkVcA5KwL2phPSv0douC0fgWzmnA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 20" } }, "node_modules/source-map": { @@ -7322,15 +7382,14 @@ } }, "node_modules/svelte": { - "version": "5.55.7", - "resolved": "https://registry.npmjs.org/svelte/-/svelte-5.55.7.tgz", - "integrity": "sha512-ymI5ykLPwIHW839E053FQbI1G+jnRFJEw3Kv5Y4njixVWywQBx+NUFpkkKyk5LIb36Fg9DVXSYpqiGekLD0hyw==", + "version": "5.56.1", + "resolved": "https://registry.npmjs.org/svelte/-/svelte-5.56.1.tgz", + "integrity": "sha512-eArsJmvl3xZVuTYD852PzIEdg2wgDdIZ1NEsIPbzAukHwi284B18No4nK2rCO9AwsWUDza4Cjvmoa4HaojTl5g==", "license": "MIT", - "peer": true, "dependencies": { "@jridgewell/remapping": "^2.3.4", "@jridgewell/sourcemap-codec": "^1.5.0", - "@sveltejs/acorn-typescript": "^1.0.5", + "@sveltejs/acorn-typescript": "^1.0.10", "@types/estree": "^1.0.5", "@types/trusted-types": "^2.0.7", "acorn": "^8.12.1", @@ -7339,7 +7398,7 @@ "clsx": "^2.1.1", "devalue": "^5.8.1", "esm-env": "^1.2.1", - "esrap": "^2.2.4", + "esrap": "^2.2.9", "is-reference": "^3.0.3", "locate-character": "^3.0.0", "magic-string": "^0.30.11", @@ -7350,9 +7409,9 @@ } }, "node_modules/svelte-check": { - "version": "4.4.5", - "resolved": "https://registry.npmjs.org/svelte-check/-/svelte-check-4.4.5.tgz", - "integrity": "sha512-1bSwIRCvvmSHrlK52fOlZmVtUZgil43jNL/2H18pRpa+eQjzGt6e3zayxhp1S7GajPFKNM/2PMCG+DZFHlG9fw==", + "version": "4.5.0", + "resolved": "https://registry.npmjs.org/svelte-check/-/svelte-check-4.5.0.tgz", + "integrity": "sha512-9lNwPxCLWniFvQIcEv1LFqjIxcFtO3smb5+5BKbRJ3ttL4o2lXCej5rLF4DAnfLPI66oaA81vAxw6ILdIWI7kA==", "dev": true, "license": "MIT", "dependencies": { @@ -7404,9 +7463,9 @@ } }, "node_modules/svelte-eslint-parser": { - "version": "1.4.0", - "resolved": "https://registry.npmjs.org/svelte-eslint-parser/-/svelte-eslint-parser-1.4.0.tgz", - "integrity": "sha512-fjPzOfipR5S7gQ/JvI9r2H8y9gMGXO3JtmrylHLLyahEMquXI0lrebcjT+9/hNgDej0H7abTyox5HpHmW1PSWA==", + "version": "1.8.0", + "resolved": "https://registry.npmjs.org/svelte-eslint-parser/-/svelte-eslint-parser-1.8.0.tgz", + "integrity": "sha512-mikR1qwIVy3t5WthUoAXkMwxkXvabZP9FJgdx35Ei7EbGWmctva1Pih16Koeor/bdNNq8NXHlwKGS6NkYTawLg==", "dev": true, "license": "MIT", "dependencies": { @@ -7415,11 +7474,12 @@ "espree": "^10.0.0", "postcss": "^8.4.49", "postcss-scss": "^4.0.9", - "postcss-selector-parser": "^7.0.0" + "postcss-selector-parser": "^7.0.0", + "semver": "^7.7.2" }, "engines": { "node": "^18.18.0 || ^20.9.0 || >=21.1.0", - "pnpm": "10.18.3" + "pnpm": "10.34.1" }, "funding": { "url": "https://github.com/sponsors/ota-meshi" @@ -7461,9 +7521,9 @@ } }, "node_modules/svelte-preprocess": { - "version": "6.0.3", - "resolved": "https://registry.npmjs.org/svelte-preprocess/-/svelte-preprocess-6.0.3.tgz", - "integrity": "sha512-PLG2k05qHdhmRG7zR/dyo5qKvakhm8IJ+hD2eFRQmMLHp7X3eJnjeupUtvuRpbNiF31RjVw45W+abDwHEmP5OA==", + "version": "6.0.5", + "resolved": "https://registry.npmjs.org/svelte-preprocess/-/svelte-preprocess-6.0.5.tgz", + "integrity": "sha512-sgwew5yV/2eMeQobIWgAxCNarKwiTUDIc3siAUbq3sp0G6ONtzk0W+wJihMdqjbYb3iGU3ubpGv0usnnuXT3qg==", "dev": true, "hasInstallScript": true, "license": "MIT", @@ -7481,7 +7541,7 @@ "stylus": ">=0.55", "sugarss": "^2.0.0 || ^3.0.0 || ^4.0.0", "svelte": "^4.0.0 || ^5.0.0-next.100 || ^5.0.0", - "typescript": "^5.0.0" + "typescript": "^5.0.0 || ^6.0.0" }, "peerDependenciesMeta": { "@babel/core": { @@ -7530,9 +7590,9 @@ } }, "node_modules/thenby": { - "version": "1.3.4", - "resolved": "https://registry.npmjs.org/thenby/-/thenby-1.3.4.tgz", - "integrity": "sha512-89Gi5raiWA3QZ4b2ePcEwswC3me9JIg+ToSgtE0JWeCynLnLxNr/f9G+xfo9K+Oj4AFdom8YNJjibIARTJmapQ==", + "version": "1.4.1", + "resolved": "https://registry.npmjs.org/thenby/-/thenby-1.4.1.tgz", + "integrity": "sha512-D5a/bO0KdalOE3q8MlrRmSxjbKZHT3MQmXkJP+r97Vw8MMwOZKOwUSEyTtK7eSMj2y0kyAjpYMRMZmmLw1FtNQ==", "dev": true, "license": "Apache-2.0" }, @@ -7610,9 +7670,9 @@ } }, "node_modules/ts-api-utils": { - "version": "2.4.0", - "resolved": "https://registry.npmjs.org/ts-api-utils/-/ts-api-utils-2.4.0.tgz", - "integrity": "sha512-3TaVTaAv2gTiMB35i3FiGJaRfwb3Pyn/j3m/bfAvGe8FB7CF6u+LMYqYlDh7reQf7UNvoTvdfAqHGmPGOSsPmA==", + "version": "2.5.0", + "resolved": "https://registry.npmjs.org/ts-api-utils/-/ts-api-utils-2.5.0.tgz", + "integrity": "sha512-OJ/ibxhPlqrMM0UiNHJ/0CKQkoKF243/AEmplt3qpRgkW8VG7IfOS41h7V8TjITqdByHzrjcS/2si+y4lIh8NA==", "dev": true, "license": "MIT", "engines": { @@ -7773,7 +7833,6 @@ "integrity": "sha512-jl1vZzPDinLr9eUt3J/t7V6FgNEw9QjvBPdysz9KfQDD41fQrC2Y4vKQdiaUpFT4bXlb1RHhLpp8wtm6M5TgSw==", "dev": true, "license": "Apache-2.0", - "peer": true, "bin": { "tsc": "bin/tsc", "tsserver": "bin/tsserver" @@ -7783,16 +7842,16 @@ } }, "node_modules/typescript-eslint": { - "version": "8.57.1", - "resolved": "https://registry.npmjs.org/typescript-eslint/-/typescript-eslint-8.57.1.tgz", - "integrity": "sha512-fLvZWf+cAGw3tqMCYzGIU6yR8K+Y9NT2z23RwOjlNFF2HwSB3KhdEFI5lSBv8tNmFkkBShSjsCjzx1vahZfISA==", + "version": "8.60.1", + "resolved": "https://registry.npmjs.org/typescript-eslint/-/typescript-eslint-8.60.1.tgz", + "integrity": "sha512-6m5hkkRAp8lKvhVpcprAIn5KkehQEh+47oHH2VGnExEh7dhNxXlg6GPAOIu6TxbVQxhebrJDvjl3020ooiWCMA==", "dev": true, "license": "MIT", "dependencies": { - "@typescript-eslint/eslint-plugin": "8.57.1", - "@typescript-eslint/parser": "8.57.1", - "@typescript-eslint/typescript-estree": "8.57.1", - "@typescript-eslint/utils": "8.57.1" + "@typescript-eslint/eslint-plugin": "8.60.1", + "@typescript-eslint/parser": "8.60.1", + "@typescript-eslint/typescript-estree": "8.60.1", + "@typescript-eslint/utils": "8.60.1" }, "engines": { "node": "^18.18.0 || ^20.9.0 || >=21.1.0" @@ -7803,7 +7862,7 @@ }, "peerDependencies": { "eslint": "^8.57.0 || ^9.0.0 || ^10.0.0", - "typescript": ">=4.8.4 <6.0.0" + "typescript": ">=4.8.4 <6.1.0" } }, "node_modules/ua-is-frozen": { @@ -7828,9 +7887,9 @@ "license": "MIT" }, "node_modules/ua-parser-js": { - "version": "2.0.9", - "resolved": "https://registry.npmjs.org/ua-parser-js/-/ua-parser-js-2.0.9.tgz", - "integrity": "sha512-OsqGhxyo/wGdLSXMSJxuMGN6H4gDnKz6Fb3IBm4bxZFMnyy0sdf6MN96Ie8tC6z/btdO+Bsy8guxlvLdwT076w==", + "version": "2.0.10", + "resolved": "https://registry.npmjs.org/ua-parser-js/-/ua-parser-js-2.0.10.tgz", + "integrity": "sha512-t+3Ktbq0Ies2vaSezfOaWiolH4OigQIO1dk+1xDpOydB1COVPocVYOrEV5rqZ0kFY9XYG1v9LutCyMgYBpABcw==", "dev": true, "funding": [ { @@ -7927,7 +7986,6 @@ "integrity": "sha512-EFrL7Hw4kmhZdwWO3dwwFJo6hO3FXuQ6Bg8BK/faHZ9m1YxqBS31BNSTxklIQkxK/4LlV8zTYnPsIRLBzTzjCA==", "dev": true, "hasInstallScript": true, - "peer": true, "dependencies": { "napi-postinstall": "^0.3.0" }, @@ -7979,7 +8037,6 @@ "integrity": "sha512-w+N7Hifpc3gRjZ63vYBXA56dvvRlNWRczTdmCBBa+CotUzAPf5b7YMdMR/8CQoeYE5LX3W4wj6RYTgonm1b9DA==", "dev": true, "license": "MIT", - "peer": true, "dependencies": { "esbuild": "^0.27.0", "fdir": "^6.5.0", diff --git a/warpgate-web/package.json b/warpgate-web/package.json index 64891ab93..8a1170ba5 100644 --- a/warpgate-web/package.json +++ b/warpgate-web/package.json @@ -25,7 +25,7 @@ "@fortawesome/free-brands-svg-icons": "^7.2.0", "@fortawesome/free-regular-svg-icons": "^7.2.0", "@fortawesome/free-solid-svg-icons": "^7.2.0", - "@openapitools/openapi-generator-cli": "^2.31.1", + "@openapitools/openapi-generator-cli": "^2.34.0", "@otplib/plugin-base32-enc-dec": "^12.0.1", "@otplib/plugin-crypto-js": "^12.0.1", "@otplib/preset-browser": "^12.0.1", @@ -41,30 +41,30 @@ "@xterm/xterm": "^6.0", "bootstrap": "^5.3.8", "copy-text-to-clipboard": "^3.2.2", - "date-fns": "^4.1.0", + "date-fns": "^4.4.0", "eslint": "^9", - "eslint-import-resolver-typescript": "^4.4.4", + "eslint-import-resolver-typescript": "^4.4.5", "eslint-plugin-import": "^2.32.0", "eslint-plugin-node": "^11.1.0", "eslint-plugin-promise": "^6", - "eslint-plugin-svelte": "^3.15.2", + "eslint-plugin-svelte": "^3.19.0", "format-duration": "^3.0.2", - "otpauth": "^9.5.0", + "otpauth": "^9.5.1", "qrcode": "^1.5.4", "rxjs": "^7.8.2", "sass": "1.78", - "svelte": "^5.55.7", - "svelte-check": "^4.4.5", + "svelte": "^5.56.1", + "svelte-check": "^4.5.0", "svelte-fa": "^4.0.4", "svelte-intersection-observer": "^1.1.1", "svelte-observable": "^0.4.0", - "svelte-preprocess": "^6.0.3", + "svelte-preprocess": "^6.0.5", "svelte-spa-router": "^4.0.1", - "thenby": "^1.3.4", + "thenby": "^1.4.1", "tslib": "^2.8.0", "typescript": "^5.9.3", - "typescript-eslint": "^8.57.1", - "ua-parser-js": "^2.0.9", + "typescript-eslint": "^8.60.1", + "ua-parser-js": "^2.0.10", "vite": "^7.3.1", "vite-tsconfig-paths": "^6.1.1", "zmodem.js": "^0.1.10" @@ -78,7 +78,7 @@ } }, "dependencies": { - "@tanstack/svelte-virtual": "^3.13.23", + "@tanstack/svelte-virtual": "^3.13.28", "natural-orderby": "^5.0.0" } } From 49f37ffa723e38e84c37656c25cb06e8abcca857 Mon Sep 17 00:00:00 2001 From: Eugene Date: Thu, 11 Jun 2026 10:47:04 +0200 Subject: [PATCH 187/556] bump version to 0.25.4 --- bumpver.toml | 2 +- helm/warpgate/Chart.yaml | 2 +- helm/warpgate/values.yaml | 2 +- warpgate-admin/Cargo.toml | 2 +- warpgate-ca/Cargo.toml | 2 +- warpgate-common-http/Cargo.toml | 2 +- warpgate-common/Cargo.toml | 2 +- warpgate-core/Cargo.toml | 2 +- warpgate-database-protocols/Cargo.toml | 2 +- warpgate-db-entities/Cargo.toml | 2 +- warpgate-db-migrations/Cargo.toml | 2 +- warpgate-ldap/Cargo.toml | 2 +- warpgate-protocol-http/Cargo.toml | 2 +- warpgate-protocol-kubernetes/Cargo.toml | 2 +- warpgate-protocol-mysql/Cargo.toml | 2 +- warpgate-protocol-postgres/Cargo.toml | 2 +- warpgate-protocol-ssh/Cargo.toml | 2 +- warpgate-sso/Cargo.toml | 2 +- warpgate-tls/Cargo.toml | 2 +- warpgate-web/Cargo.toml | 2 +- warpgate/Cargo.toml | 2 +- 21 files changed, 21 insertions(+), 21 deletions(-) diff --git a/bumpver.toml b/bumpver.toml index 2c57b8a1b..0a4c88a7c 100644 --- a/bumpver.toml +++ b/bumpver.toml @@ -1,5 +1,5 @@ [bumpver] -current_version = "0.25.3" +current_version = "0.25.4" version_pattern = "MAJOR.MINOR.PATCH[-TAG[.INC0]]" commit = true tag = false diff --git a/helm/warpgate/Chart.yaml b/helm/warpgate/Chart.yaml index 6835250b5..15d846910 100644 --- a/helm/warpgate/Chart.yaml +++ b/helm/warpgate/Chart.yaml @@ -22,4 +22,4 @@ version: 0.0.2 # incremented each time you make changes to the application. Versions are not expected to # follow Semantic Versioning. They should reflect the version the application is using. # It is recommended to use it with quotes. -appVersion: "0.25.3" +appVersion: "0.25.4" diff --git a/helm/warpgate/values.yaml b/helm/warpgate/values.yaml index c616257e7..3b3c9f0c0 100644 --- a/helm/warpgate/values.yaml +++ b/helm/warpgate/values.yaml @@ -5,7 +5,7 @@ replicaCount: 1 image: repository: ghcr.io/warp-tech/warpgate pullPolicy: IfNotPresent - tag: "0.25.3" + tag: "0.25.4" # References to Kubernetes secrets for pulling images (if using a private registry) imagePullSecrets: [] diff --git a/warpgate-admin/Cargo.toml b/warpgate-admin/Cargo.toml index 4bb565dc7..2d5629ac3 100644 --- a/warpgate-admin/Cargo.toml +++ b/warpgate-admin/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "warpgate-admin" -version = "0.25.3" +version = "0.25.4" edition = "2024" license = "Apache-2.0" publish = false diff --git a/warpgate-ca/Cargo.toml b/warpgate-ca/Cargo.toml index 4509fb6b4..66f908f50 100644 --- a/warpgate-ca/Cargo.toml +++ b/warpgate-ca/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "warpgate-ca" -version = "0.25.3" +version = "0.25.4" edition = "2024" license = "Apache-2.0" publish = false diff --git a/warpgate-common-http/Cargo.toml b/warpgate-common-http/Cargo.toml index c3ac4ee06..8ce4b9f2b 100644 --- a/warpgate-common-http/Cargo.toml +++ b/warpgate-common-http/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "warpgate-common-http" -version = "0.25.3" +version = "0.25.4" edition = "2024" license = "Apache-2.0" publish = false diff --git a/warpgate-common/Cargo.toml b/warpgate-common/Cargo.toml index 6b6c7da5a..86ac33a2a 100644 --- a/warpgate-common/Cargo.toml +++ b/warpgate-common/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "warpgate-common" -version = "0.25.3" +version = "0.25.4" edition = "2024" license = "Apache-2.0" publish = false diff --git a/warpgate-core/Cargo.toml b/warpgate-core/Cargo.toml index 6dc4f7d47..da5447a9f 100644 --- a/warpgate-core/Cargo.toml +++ b/warpgate-core/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "warpgate-core" -version = "0.25.3" +version = "0.25.4" edition = "2024" license = "Apache-2.0" publish = false diff --git a/warpgate-database-protocols/Cargo.toml b/warpgate-database-protocols/Cargo.toml index 682d7246a..43a4072ac 100644 --- a/warpgate-database-protocols/Cargo.toml +++ b/warpgate-database-protocols/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "warpgate-database-protocols" -version = "0.25.3" +version = "0.25.4" description = "Core of SQLx, the rust SQL toolkit. Just the database protocol parts." authors = [ "Ryan Leckey ", diff --git a/warpgate-db-entities/Cargo.toml b/warpgate-db-entities/Cargo.toml index 08534c131..8554b22e9 100644 --- a/warpgate-db-entities/Cargo.toml +++ b/warpgate-db-entities/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "warpgate-db-entities" -version = "0.25.3" +version = "0.25.4" edition = "2024" license = "Apache-2.0" publish = false diff --git a/warpgate-db-migrations/Cargo.toml b/warpgate-db-migrations/Cargo.toml index 996befba9..9706f4971 100644 --- a/warpgate-db-migrations/Cargo.toml +++ b/warpgate-db-migrations/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "warpgate-db-migrations" -version = "0.25.3" +version = "0.25.4" edition = "2024" license = "Apache-2.0" publish = false diff --git a/warpgate-ldap/Cargo.toml b/warpgate-ldap/Cargo.toml index 7063243c3..7cfe697a1 100644 --- a/warpgate-ldap/Cargo.toml +++ b/warpgate-ldap/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "warpgate-ldap" -version = "0.25.3" +version = "0.25.4" edition = "2024" license = "Apache-2.0" publish = false diff --git a/warpgate-protocol-http/Cargo.toml b/warpgate-protocol-http/Cargo.toml index ccca8f844..e71165351 100644 --- a/warpgate-protocol-http/Cargo.toml +++ b/warpgate-protocol-http/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "warpgate-protocol-http" -version = "0.25.3" +version = "0.25.4" edition = "2024" license = "Apache-2.0" publish = false diff --git a/warpgate-protocol-kubernetes/Cargo.toml b/warpgate-protocol-kubernetes/Cargo.toml index 89dcf4012..8adb95d8e 100644 --- a/warpgate-protocol-kubernetes/Cargo.toml +++ b/warpgate-protocol-kubernetes/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "warpgate-protocol-kubernetes" -version = "0.25.3" +version = "0.25.4" edition = "2024" license = "Apache-2.0" publish = false diff --git a/warpgate-protocol-mysql/Cargo.toml b/warpgate-protocol-mysql/Cargo.toml index 88b3429a3..4f8aef000 100644 --- a/warpgate-protocol-mysql/Cargo.toml +++ b/warpgate-protocol-mysql/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "warpgate-protocol-mysql" -version = "0.25.3" +version = "0.25.4" edition = "2024" license = "Apache-2.0" publish = false diff --git a/warpgate-protocol-postgres/Cargo.toml b/warpgate-protocol-postgres/Cargo.toml index 5fc3677a6..5782def06 100644 --- a/warpgate-protocol-postgres/Cargo.toml +++ b/warpgate-protocol-postgres/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "warpgate-protocol-postgres" -version = "0.25.3" +version = "0.25.4" edition = "2024" license = "Apache-2.0" publish = false diff --git a/warpgate-protocol-ssh/Cargo.toml b/warpgate-protocol-ssh/Cargo.toml index bf147a564..5bff7fc79 100644 --- a/warpgate-protocol-ssh/Cargo.toml +++ b/warpgate-protocol-ssh/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "warpgate-protocol-ssh" -version = "0.25.3" +version = "0.25.4" edition = "2024" license = "Apache-2.0" publish = false diff --git a/warpgate-sso/Cargo.toml b/warpgate-sso/Cargo.toml index 3e935b317..1b179f627 100644 --- a/warpgate-sso/Cargo.toml +++ b/warpgate-sso/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "warpgate-sso" -version = "0.25.3" +version = "0.25.4" edition = "2024" license = "Apache-2.0" publish = false diff --git a/warpgate-tls/Cargo.toml b/warpgate-tls/Cargo.toml index 13a42e9aa..7c1f638b4 100644 --- a/warpgate-tls/Cargo.toml +++ b/warpgate-tls/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "warpgate-tls" -version = "0.25.3" +version = "0.25.4" edition = "2024" license = "Apache-2.0" publish = false diff --git a/warpgate-web/Cargo.toml b/warpgate-web/Cargo.toml index 18a2d9647..dc641b72d 100644 --- a/warpgate-web/Cargo.toml +++ b/warpgate-web/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "warpgate-web" -version = "0.25.3" +version = "0.25.4" edition = "2024" license = "Apache-2.0" diff --git a/warpgate/Cargo.toml b/warpgate/Cargo.toml index 2be5c66cf..815e4c7c9 100644 --- a/warpgate/Cargo.toml +++ b/warpgate/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "warpgate" -version = "0.25.3" +version = "0.25.4" edition = "2024" license = "Apache-2.0" publish = false From 54e6e2940ee99fe62b4b199af9c66aeb89b68e86 Mon Sep 17 00:00:00 2001 From: Eugene Date: Thu, 11 Jun 2026 10:47:20 +0200 Subject: [PATCH 188/556] Update Cargo.lock --- Cargo.lock | 36 ++++++++++++++++++------------------ 1 file changed, 18 insertions(+), 18 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index 80479ebe0..52e5f951e 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -7781,7 +7781,7 @@ dependencies = [ [[package]] name = "warpgate" -version = "0.25.3" +version = "0.25.4" dependencies = [ "anyhow", "async-trait", @@ -7824,7 +7824,7 @@ dependencies = [ [[package]] name = "warpgate-admin" -version = "0.25.3" +version = "0.25.4" dependencies = [ "anyhow", "async-trait", @@ -7884,7 +7884,7 @@ dependencies = [ [[package]] name = "warpgate-ca" -version = "0.25.3" +version = "0.25.4" dependencies = [ "aws-lc-rs", "bytes", @@ -7905,7 +7905,7 @@ dependencies = [ [[package]] name = "warpgate-common" -version = "0.25.3" +version = "0.25.4" dependencies = [ "anyhow", "argon2 0.5.3", @@ -7958,7 +7958,7 @@ dependencies = [ [[package]] name = "warpgate-common-http" -version = "0.25.3" +version = "0.25.4" dependencies = [ "poem", "poem-openapi", @@ -7973,7 +7973,7 @@ dependencies = [ [[package]] name = "warpgate-core" -version = "0.25.3" +version = "0.25.4" dependencies = [ "anyhow", "argon2 0.5.3", @@ -8018,7 +8018,7 @@ dependencies = [ [[package]] name = "warpgate-database-protocols" -version = "0.25.3" +version = "0.25.4" dependencies = [ "bitflags 2.13.0", "bytes", @@ -8031,7 +8031,7 @@ dependencies = [ [[package]] name = "warpgate-db-entities" -version = "0.25.3" +version = "0.25.4" dependencies = [ "bytes", "ipnet", @@ -8050,7 +8050,7 @@ dependencies = [ [[package]] name = "warpgate-db-migrations" -version = "0.25.3" +version = "0.25.4" dependencies = [ "data-encoding", "regex", @@ -8068,7 +8068,7 @@ dependencies = [ [[package]] name = "warpgate-ldap" -version = "0.25.3" +version = "0.25.4" dependencies = [ "anyhow", "ldap3", @@ -8084,7 +8084,7 @@ dependencies = [ [[package]] name = "warpgate-protocol-http" -version = "0.25.3" +version = "0.25.4" dependencies = [ "anyhow", "async-trait", @@ -8123,7 +8123,7 @@ dependencies = [ [[package]] name = "warpgate-protocol-kubernetes" -version = "0.25.3" +version = "0.25.4" dependencies = [ "anyhow", "async-trait", @@ -8162,7 +8162,7 @@ dependencies = [ [[package]] name = "warpgate-protocol-mysql" -version = "0.25.3" +version = "0.25.4" dependencies = [ "anyhow", "async-trait", @@ -8190,7 +8190,7 @@ dependencies = [ [[package]] name = "warpgate-protocol-postgres" -version = "0.25.3" +version = "0.25.4" dependencies = [ "anyhow", "async-trait", @@ -8217,7 +8217,7 @@ dependencies = [ [[package]] name = "warpgate-protocol-ssh" -version = "0.25.3" +version = "0.25.4" dependencies = [ "anyhow", "async-trait", @@ -8250,7 +8250,7 @@ dependencies = [ [[package]] name = "warpgate-sso" -version = "0.25.3" +version = "0.25.4" dependencies = [ "bytes", "data-encoding", @@ -8270,7 +8270,7 @@ dependencies = [ [[package]] name = "warpgate-tls" -version = "0.25.3" +version = "0.25.4" dependencies = [ "poem", "poem-openapi", @@ -8290,7 +8290,7 @@ dependencies = [ [[package]] name = "warpgate-web" -version = "0.25.3" +version = "0.25.4" dependencies = [ "rust-embed", "serde", From db72205528df8cef1cd1389a27dc3c003f5afe25 Mon Sep 17 00:00:00 2001 From: Eugene Date: Fri, 12 Jun 2026 21:45:44 +0200 Subject: [PATCH 189/556] bump postgres-protocol crate --- Cargo.lock | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index 52e5f951e..72556b089 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -4018,7 +4018,7 @@ version = "5.0.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "51e219e79014df21a225b1860a479e2dcd7cbd9130f4defd4bd0e191ea31d67d" dependencies = [ - "base64 0.22.1", + "base64 0.21.7", "chrono", "getrandom 0.2.17", "http 1.4.1", @@ -4807,9 +4807,9 @@ checksum = "c33a9471896f1c69cecef8d20cbe2f7accd12527ce60845ff44c153bb2a21b49" [[package]] name = "postgres-protocol" -version = "0.6.11" +version = "0.6.12" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "56201207dac53e2f38e848e31b4b91616a6bb6e0c7205b77718994a7f49e70fc" +checksum = "08808e3c483c46e999108051c78334f473d5adb59d78bb80a1268c7e6aa6c514" dependencies = [ "base64 0.22.1", "byteorder", @@ -6954,7 +6954,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "32497e9a4c7b38532efcdebeef879707aa9f794296a4f0244f6f69e9bc8574bd" dependencies = [ "fastrand", - "getrandom 0.4.2", + "getrandom 0.3.4", "once_cell", "rustix 1.1.4", "windows-sys 0.61.2", From e83fc038e8605b7cab47e19a127f28a356cb943a Mon Sep 17 00:00:00 2001 From: Eugene Date: Fri, 12 Jun 2026 21:57:43 +0200 Subject: [PATCH 190/556] #1989 - return 401 instead of a redirect for fetch (#2060) --- warpgate-protocol-http/src/common.rs | 42 +++++++++++++++++++++++++++- 1 file changed, 41 insertions(+), 1 deletion(-) diff --git a/warpgate-protocol-http/src/common.rs b/warpgate-protocol-http/src/common.rs index 5d13a2308..724b1a920 100644 --- a/warpgate-protocol-http/src/common.rs +++ b/warpgate-protocol-http/src/common.rs @@ -169,6 +169,15 @@ pub fn page_auth(e: E) -> impl Endpoint { } pub fn gateway_redirect(req: &Request) -> Response { + // Only do a login redirect for document requests + if let Some(mode) = req.headers().get(HeaderName::from_static("sec-fetch-mode")) + && mode != "navigate" + { + return Response::builder() + .status(StatusCode::UNAUTHORIZED) + .finish(); + } + let path = req .original_uri() .path_and_query() @@ -388,7 +397,38 @@ pub async fn inject_request_authorization( #[cfg(test)] mod tests { - use super::host_is_subdomain_of_or_equal; + use super::{StatusCode, gateway_redirect, host_is_subdomain_of_or_equal}; + + #[test] + fn gateway_redirect_navigation_redirects_to_login() { + for mode in [None, Some("navigate")] { + let mut req = poem::Request::builder().uri_str("/api/data"); + if let Some(mode) = mode { + req = req.header("sec-fetch-mode", mode); + } + let resp = gateway_redirect(&req.finish()); + assert_eq!(resp.status(), StatusCode::TEMPORARY_REDIRECT); + let location = resp + .headers() + .get(http::header::LOCATION) + .and_then(|v| v.to_str().ok()) + .unwrap_or_default(); + assert!(location.starts_with("/@warpgate#/login")); + } + } + + #[test] + fn gateway_redirect_fetch_gets_401() { + // https://github.com/warp-tech/warpgate/issues/1989 + for mode in ["cors", "same-origin", "no-cors"] { + let req = poem::Request::builder() + .uri_str("/api/data") + .header("sec-fetch-mode", mode) + .finish(); + let resp = gateway_redirect(&req); + assert_eq!(resp.status(), StatusCode::UNAUTHORIZED); + } + } #[test] fn test_host_is_subdomain_of_or_equal() { From e2cbd643487976798cd3390338cfcb88bd53f685 Mon Sep 17 00:00:00 2001 From: Eugene Date: Fri, 12 Jun 2026 22:05:19 +0200 Subject: [PATCH 191/556] fixed #2048 - strip cookie domains (#2061) --- warpgate-protocol-http/src/proxy.rs | 48 +++++++++++++++++++++++++++++ 1 file changed, 48 insertions(+) diff --git a/warpgate-protocol-http/src/proxy.rs b/warpgate-protocol-http/src/proxy.rs index 691fcf7af..5030c29b2 100644 --- a/warpgate-protocol-http/src/proxy.rs +++ b/warpgate-protocol-http/src/proxy.rs @@ -182,6 +182,9 @@ fn rewrite_response( try_block!({ let mut cookie = Cookie::parse(value.to_str()?)?; cookie.set_expires(cookie::Expiration::Session); + // the domain set by the target isn't going to match the actual host anyway + // https://github.com/warp-tech/warpgate/issues/2048 + cookie.unset_domain(); *value = cookie.to_string().parse()?; } catch (error: anyhow::Error) { warn!(?error, header=?value, "Failed to parse response cookie"); @@ -523,3 +526,48 @@ async fn proxy_ws_inner( rewrite_response(&mut response, options, &uri)?; Ok(response) } + +#[cfg(test)] +mod tests { + use super::*; + + fn make_options(url: &str) -> TargetHTTPOptions { + TargetHTTPOptions { + url: url.to_string(), + tls: Default::default(), + headers: None, + external_host: None, + } + } + + #[test] + fn rewrite_response_strips_cookie_domain() { + let mut resp = poem::Response::builder() + .header( + http::header::SET_COOKIE, + "lsws_uid=abc; HttpOnly; Secure; Path=/; Domain=100.0.0.1", + ) + .body(()); + + let options = make_options("https://100.0.0.1:7080"); + let source_uri = Uri::try_from("https://100.0.0.1:7080/login.php").unwrap(); + + rewrite_response(&mut resp, &options, &source_uri).unwrap(); + + let cookie_headers: Vec<_> = resp + .headers() + .get_all(http::header::SET_COOKIE) + .iter() + .map(|v| v.to_str().unwrap().to_string()) + .collect(); + + assert_eq!(cookie_headers.len(), 1); + let cookie = Cookie::parse(cookie_headers[0].as_str()).unwrap(); + assert_eq!(cookie.name(), "lsws_uid"); + assert_eq!(cookie.value(), "abc"); + assert_eq!(cookie.domain(), None); + assert_eq!(cookie.path(), Some("/")); + assert_eq!(cookie.http_only(), Some(true)); + assert_eq!(cookie.secure(), Some(true)); + } +} From 0d8321a4bf711bd71c50bcb5bd36977d34ff2578 Mon Sep 17 00:00:00 2001 From: Eugene Date: Fri, 12 Jun 2026 22:18:18 +0200 Subject: [PATCH 192/556] fixed #2049 - web ssh sessions never marked as ended (#2062) --- warpgate-web-ssh/src/manager.rs | 14 ++++++++++---- 1 file changed, 10 insertions(+), 4 deletions(-) diff --git a/warpgate-web-ssh/src/manager.rs b/warpgate-web-ssh/src/manager.rs index 622b39822..5590c4e78 100644 --- a/warpgate-web-ssh/src/manager.rs +++ b/warpgate-web-ssh/src/manager.rs @@ -110,10 +110,14 @@ impl WebSshClientManager { services.recordings.clone(), )); + // weak ref to avoid the ref cycle + // https://github.com/warp-tech/warpgate/issues/2049 tokio::spawn({ - let session = session.clone(); + let session = Arc::downgrade(&session); async move { - if abort_rx.recv().await.is_some() { + if abort_rx.recv().await.is_some() + && let Some(session) = session.upgrade() + { session.close(); } } @@ -281,13 +285,15 @@ fn spawn_event_loop( } } RCEvent::Done => { - session.close(); - sessions.lock().await.remove(&session.id()); break; } _ => {} } } + + // remote client is gone now + session.close(); + sessions.lock().await.remove(&session.id()); anyhow::Ok(()) } .instrument(span), From 8d5df96437ab990a84cabacdc9e242496e39143a Mon Sep 17 00:00:00 2001 From: Eugene Date: Sat, 13 Jun 2026 01:02:15 +0200 Subject: [PATCH 193/556] fixed #2050 - SSH target menu freezing when running in Docker (#2063) --- warpgate-protocol-ssh/src/server/session.rs | 16 +- .../src/server/target_menu.rs | 329 +++++++++++++----- 2 files changed, 258 insertions(+), 87 deletions(-) diff --git a/warpgate-protocol-ssh/src/server/session.rs b/warpgate-protocol-ssh/src/server/session.rs index d3ef9c5af..d900f7965 100644 --- a/warpgate-protocol-ssh/src/server/session.rs +++ b/warpgate-protocol-ssh/src/server/session.rs @@ -410,15 +410,17 @@ impl ServerSession { &self, ssh_chain: &[ResolvedSshChainHost], ) -> Result, WarpgateError> { - let mut display = vec![ - VisualConnectionChainItem::Text("You".into()), - VisualConnectionChainItem::Link { + let maybe_ext_url = + construct_external_url(None, &*self.services.config.lock().await, None).await; + let warpgate_item = match maybe_ext_url { + Ok(url) => VisualConnectionChainItem::Link { text: "Warpgate".into(), - url: construct_external_url(None, &*self.services.config.lock().await, None) - .await? - .to_string(), + url: url.to_string(), }, - ]; + Err(_) => VisualConnectionChainItem::Text("Warpgate".into()), + }; + + let mut display = vec![VisualConnectionChainItem::Text("You".into()), warpgate_item]; display.extend( ssh_chain .iter() diff --git a/warpgate-protocol-ssh/src/server/target_menu.rs b/warpgate-protocol-ssh/src/server/target_menu.rs index ddb1d5107..2f91f40da 100644 --- a/warpgate-protocol-ssh/src/server/target_menu.rs +++ b/warpgate-protocol-ssh/src/server/target_menu.rs @@ -1,9 +1,10 @@ -use std::io::Cursor; +use std::io::{self, Cursor}; use std::ops::Deref; use bytes::Bytes; -use ratatui::backend::CrosstermBackend; -use ratatui::layout::{Constraint, Layout, Rect}; +use ratatui::backend::{Backend, ClearType, CrosstermBackend, WindowSize}; +use ratatui::buffer::Cell; +use ratatui::layout::{Constraint, Layout, Position, Rect, Size}; use ratatui::style::{Color, Modifier, Style, Stylize}; use ratatui::text::Line; use ratatui::widgets::{ @@ -19,7 +20,99 @@ use crate::server::session::Event; const HEADER_HEIGHT: u16 = 6; -type MenuTerminal = Terminal>>>; +type MenuTerminal = Terminal; + +/// A virtual backend that renders to a buffer and fakes a terminal size +/// need this because `CrosstermBackend` checks PTY size by querying the actual local PTY +struct VirtualTerminalBackend { + inner: CrosstermBackend>>, + size: Size, + cursor_position: Position, +} + +impl VirtualTerminalBackend { + const fn new(size: Size) -> Self { + Self { + inner: CrosstermBackend::new(Cursor::new(Vec::new())), + size, + cursor_position: Position::ORIGIN, + } + } + + const fn set_size(&mut self, size: Size) { + self.size = size; + } + + /// Returns all output produced since the last call and resets the buffer. + fn take_output(&mut self) -> Vec { + let writer = self.inner.writer_mut(); + writer.set_position(0); + std::mem::take(writer.get_mut()) + } +} + +impl Backend for VirtualTerminalBackend { + type Error = io::Error; + + fn draw<'a, I>(&mut self, content: I) -> io::Result<()> + where + I: Iterator, + { + let mut last_cell: Option<(u16, u16)> = None; + self.inner + .draw(content.inspect(|(x, y, _)| last_cell = Some((*x, *y))))?; + + // Cursor is now behind the last drawn cell + if let Some((x, y)) = last_cell { + self.cursor_position = Position { + x: x.saturating_add(1).min(self.size.width.saturating_sub(1)), + y, + }; + } + Ok(()) + } + + fn hide_cursor(&mut self) -> io::Result<()> { + self.inner.hide_cursor() + } + + fn show_cursor(&mut self) -> io::Result<()> { + self.inner.show_cursor() + } + + fn get_cursor_position(&mut self) -> io::Result { + Ok(self.cursor_position) + } + + fn set_cursor_position>(&mut self, position: P) -> io::Result<()> { + let position = position.into(); + self.cursor_position = position; + self.inner.set_cursor_position(position) + } + + fn clear(&mut self) -> io::Result<()> { + self.inner.clear() + } + + fn clear_region(&mut self, clear_type: ClearType) -> io::Result<()> { + self.inner.clear_region(clear_type) + } + + fn size(&self) -> io::Result { + Ok(self.size) + } + + fn window_size(&mut self) -> io::Result { + Ok(WindowSize { + columns_rows: self.size, + pixels: Size::new(0, 0), + }) + } + + fn flush(&mut self) -> io::Result<()> { + self.inner.flush() + } +} struct DrawState { list_state: ListState, @@ -70,7 +163,7 @@ impl TargetMenu { ) -> Result { entries.sort_by(|a, b| a.label.cmp(&b.label)); let terminal = Terminal::with_options( - CrosstermBackend::new(Cursor::new(Vec::new())), + VirtualTerminalBackend::new(Size::new(terminal_width, terminal_height)), TerminalOptions { viewport: Viewport::Fixed(Rect::default()), }, @@ -200,13 +293,8 @@ impl TargetMenu { let area = Rect::new(0, 0, self.terminal_width, self.terminal_height); - { - let w = self.terminal.backend_mut().writer_mut(); - w.get_mut().clear(); - w.set_position(0); - } - if area != self.last_area { + self.terminal.backend_mut().set_size(area.as_size()); self.terminal.resize(area)?; self.last_area = area; } @@ -301,7 +389,7 @@ impl TargetMenu { self.list_state = draw_state.list_state; - let bytes = self.terminal.backend().writer().get_ref().clone(); + let bytes = self.terminal.backend_mut().take_output(); String::from_utf8(bytes).map_err(WarpgateError::other) } @@ -372,13 +460,9 @@ impl TargetMenu { /// restore terminal state pub fn cleanup(&mut self) -> Result { - { - let w = self.terminal.backend_mut().writer_mut(); - w.get_mut().clear(); - w.set_position(0); - } + let _ = self.terminal.backend_mut().take_output(); self.terminal.show_cursor()?; - let bytes = self.terminal.backend().writer().get_ref().clone(); + let bytes = self.terminal.backend_mut().take_output(); String::from_utf8(bytes).map_err(WarpgateError::other) } } @@ -394,86 +478,171 @@ pub fn spawn_target_menu_loop( ) -> anyhow::Result<()> { let name = format!("SSH {id} target menu loop"); tokio::task::Builder::new().name(&name).spawn(async move { - let mut menu = TargetMenu::new( - entries - .into_iter() - .map(|(target, options)| MenuEntry { - label: target.name.clone(), - value: (target, options), - }) - .collect(), + if let Err(error) = run_target_menu_loop( + entries, username, + &mut subscription, + &sender, terminal_width, terminal_height, - )?; - - if sender - .send_once(Event::Menu(MenuEvent::Render(Bytes::from(menu.render()?)))) - .await - .is_err() + ) + .await { - return Ok::<(), WarpgateError>(()); + tracing::error!(?error, "Target menu error"); + let _ = sender.send_once(Event::Menu(MenuEvent::Abort)).await; } + })?; + + Ok(()) +} - while let Some(event) = subscription.recv().await { - match event { - Event::MenuRedraw(new_width, new_height) => { - menu.terminal_width = new_width; - menu.terminal_height = new_height; +async fn run_target_menu_loop( + entries: Vec<(Target, TargetSSHOptions)>, + username: String, + subscription: &mut EventSubscription, + sender: &EventSender, + terminal_width: u16, + terminal_height: u16, +) -> Result<(), WarpgateError> { + let mut menu = TargetMenu::new( + entries + .into_iter() + .map(|(target, options)| MenuEntry { + label: target.name.clone(), + value: (target, options), + }) + .collect(), + username, + terminal_width, + terminal_height, + )?; + + if sender + .send_once(Event::Menu(MenuEvent::Render(Bytes::from(menu.render()?)))) + .await + .is_err() + { + return Ok(()); + } + + while let Some(event) = subscription.recv().await { + match event { + Event::MenuRedraw(new_width, new_height) => { + menu.terminal_width = new_width; + menu.terminal_height = new_height; + if sender + .send_once(Event::Menu(MenuEvent::Render(Bytes::from(menu.render()?)))) + .await + .is_err() + { + break; + } + } + Event::ConsoleInput(data) => { + let action = match menu.handle_input(&data) { + None => None, + Some(MenuInputResult::Redraw) => { + Some(MenuEvent::Render(Bytes::from(menu.render()?))) + } + Some(MenuInputResult::Abort) => Some(MenuEvent::Abort), + Some(MenuInputResult::Selected((target, _options))) => { + Some(MenuEvent::Selected(target)) + } + }; + + let terminal = matches!(action, Some(MenuEvent::Selected(..) | MenuEvent::Abort)); + + if terminal { + // restore terminal state if sender - .send_once(Event::Menu(MenuEvent::Render(Bytes::from(menu.render()?)))) + .send_once(Event::Menu(MenuEvent::Render(Bytes::from(menu.cleanup()?)))) .await .is_err() { break; } } - Event::ConsoleInput(data) => { - let action = match menu.handle_input(&data) { - None => None, - Some(MenuInputResult::Redraw) => { - Some(MenuEvent::Render(Bytes::from(menu.render()?))) - } - Some(MenuInputResult::Abort) => Some(MenuEvent::Abort), - Some(MenuInputResult::Selected((target, _options))) => { - Some(MenuEvent::Selected(target)) - } - }; - - let terminal = - matches!(action, Some(MenuEvent::Selected(..) | MenuEvent::Abort)); - - if terminal { - // restore terminal state - if sender - .send_once(Event::Menu(MenuEvent::Render(Bytes::from(menu.cleanup()?)))) - .await - .is_err() - { - break; - } - } - if let Some(action) = action - && sender.send_once(Event::Menu(action)).await.is_err() - { - break; - } + if let Some(action) = action + && sender.send_once(Event::Menu(action)).await.is_err() + { + break; + } - if terminal { - break; - } + if terminal { + break; } - Event::Command(_) - | Event::ServerHandler(_) - | Event::ServiceOutput(_) - | Event::Client(_) - | Event::Menu(_) => {} } + Event::Command(_) + | Event::ServerHandler(_) + | Event::ServiceOutput(_) + | Event::Client(_) + | Event::Menu(_) => {} } - - Ok(()) - })?; + } Ok(()) } + +#[cfg(test)] +mod tests { + use super::*; + + fn make_menu() -> TargetMenu { + // Must work without a local TTY (Warpgate normally runs without one) - + // this is what regressed in #2050. + TargetMenu::new( + vec![ + MenuEntry { + label: "alpha".into(), + value: 1, + }, + MenuEntry { + label: "bravo".into(), + value: 2, + }, + ], + "user".into(), + 120, + 40, + ) + .expect("menu creation should not require a local TTY") + } + + #[test] + fn renders_without_a_local_tty() { + let mut menu = make_menu(); + let out = menu + .render() + .expect("rendering should not require a local TTY"); + assert!(out.contains("alpha")); + assert!(out.contains("bravo")); + assert!(out.contains("Warpgate")); + } + + #[test] + fn handles_input_and_selection() { + let mut menu = make_menu(); + menu.render().expect("initial render failed"); + + // Down arrow + let result = menu.handle_input(b"\x1b[B"); + assert!(matches!(result, Some(MenuInputResult::Redraw))); + menu.render().expect("redraw failed"); + + // Enter selects the second entry + let result = menu.handle_input(b"\r"); + assert!(matches!(result, Some(MenuInputResult::Selected(2)))); + } + + #[test] + fn resize_renders_full_frame() { + let mut menu = make_menu(); + menu.render().expect("initial render failed"); + + menu.terminal_width = 80; + menu.terminal_height = 24; + let out = menu.render().expect("render after resize failed"); + assert!(out.contains("alpha")); + } +} From ea60431574dce317f62763714025ff9408d5b963 Mon Sep 17 00:00:00 2001 From: Eugene Date: Thu, 18 Jun 2026 15:25:03 +0200 Subject: [PATCH 194/556] fixed #1957 - don't offer credentials for disabled SSH auth methods (#2071) --- warpgate-protocol-ssh/src/server/mod.rs | 5 ++++ warpgate-protocol-ssh/src/server/session.rs | 26 ++++++++++++++++----- 2 files changed, 25 insertions(+), 6 deletions(-) diff --git a/warpgate-protocol-ssh/src/server/mod.rs b/warpgate-protocol-ssh/src/server/mod.rs index d679e1b9d..756a0a12f 100644 --- a/warpgate-protocol-ssh/src/server/mod.rs +++ b/warpgate-protocol-ssh/src/server/mod.rs @@ -193,6 +193,11 @@ pub async fn get_allowed_auth_methods(services: &Services) -> Result warn!( "All SSH authentication methods are disabled in parameters. Enabling all methods as fallback." ); + methods_vec = vec![ + MethodKind::PublicKey, + MethodKind::Password, + MethodKind::KeyboardInteractive, + ]; } Ok(MethodSet::from(&methods_vec[..])) diff --git a/warpgate-protocol-ssh/src/server/session.rs b/warpgate-protocol-ssh/src/server/session.rs index d900f7965..85369b3e0 100644 --- a/warpgate-protocol-ssh/src/server/session.rs +++ b/warpgate-protocol-ssh/src/server/session.rs @@ -259,6 +259,25 @@ impl ServerSession { self.main_event_subscription.recv().await } + /// Based on the global params (#1957) + fn supported_credential_kinds(&self) -> Vec { + let mut kinds = vec![]; + if self.allowed_auth_methods.contains(&MethodKind::Password) { + kinds.push(CredentialKind::Password); + } + if self.allowed_auth_methods.contains(&MethodKind::PublicKey) { + kinds.push(CredentialKind::PublicKey); + } + if self + .allowed_auth_methods + .contains(&MethodKind::KeyboardInteractive) + { + kinds.push(CredentialKind::Totp); + kinds.push(CredentialKind::WebUserApproval); + } + kinds + } + async fn get_auth_state(&mut self, username: &str) -> Result>> { #[allow(clippy::unwrap_used)] if self.auth_state.is_none() @@ -281,12 +300,7 @@ impl ServerSession { Some(&self.id), username, crate::PROTOCOL_NAME, - &[ - CredentialKind::Password, - CredentialKind::PublicKey, - CredentialKind::Totp, - CredentialKind::WebUserApproval, - ], + &self.supported_credential_kinds(), Some(self.remote_address.ip()), ) .await? From 03659720cb7976724674b81a9eaaacd2dc1cd8a2 Mon Sep 17 00:00:00 2001 From: Eugene Date: Thu, 18 Jun 2026 15:25:11 +0200 Subject: [PATCH 195/556] #1962 - handle connection accept errors gracefully (#2072) --- warpgate-common/src/types/listen_endpoint.rs | 19 +++++++++-- warpgate-protocol-mysql/src/lib.rs | 11 +++--- warpgate-protocol-postgres/src/lib.rs | 35 ++++++++++++++------ warpgate-protocol-ssh/src/server/mod.rs | 4 +-- 4 files changed, 50 insertions(+), 19 deletions(-) diff --git a/warpgate-common/src/types/listen_endpoint.rs b/warpgate-common/src/types/listen_endpoint.rs index ea4370683..5c1b23e1f 100644 --- a/warpgate-common/src/types/listen_endpoint.rs +++ b/warpgate-common/src/types/listen_endpoint.rs @@ -1,6 +1,7 @@ use std::fmt::Debug; use std::io::ErrorKind; use std::net::{Ipv4Addr, Ipv6Addr, SocketAddr, ToSocketAddrs}; +use std::time::Duration; use futures::stream::{FuturesUnordered, iter}; use futures::{Stream, StreamExt, TryStreamExt}; @@ -9,6 +10,7 @@ use schemars::JsonSchema; use serde::{Deserialize, Serialize}; use tokio::net::{TcpListener, TcpStream}; use tokio_stream::wrappers::TcpListenerStream; +use tracing::warn; use crate::WarpgateError; @@ -67,14 +69,27 @@ impl ListenEndpoint { pub async fn tcp_accept_stream( &self, - ) -> Result>, WarpgateError> { + ) -> Result + Unpin + Send, WarpgateError> { Ok(iter( self.tcp_listeners() .await? .into_iter() .map(TcpListenerStream::new), ) - .flatten_unordered(None)) + .flatten_unordered(None) + .filter_map(|result| async { + match result { + Ok(stream) => Some(stream), + Err(error) => { + // #1962 - accept errors as transient (e.g. EMFILE) + // and don't kill the listener + warn!(%error, "Failed to accept connection"); + tokio::time::sleep(Duration::from_millis(100)).await; + None + } + } + }) + .boxed()) } pub const fn port(&self) -> u16 { diff --git a/warpgate-protocol-mysql/src/lib.rs b/warpgate-protocol-mysql/src/lib.rs index 909646734..c25fe6e3d 100644 --- a/warpgate-protocol-mysql/src/lib.rs +++ b/warpgate-protocol-mysql/src/lib.rs @@ -8,7 +8,7 @@ use std::fmt::Debug; use std::sync::Arc; use anyhow::{Context, Result}; -use futures::TryStreamExt; +use futures::StreamExt; use rustls::ServerConfig; use rustls::server::NoClientAuth; use tracing::{Instrument, error, info, warn}; @@ -69,12 +69,15 @@ impl ProtocolServer for MySQLProtocolServer { let mut listener = address.tcp_accept_stream().await?; loop { - let Some(stream) = listener.try_next().await.context("accepting connection")? else { + let Some(stream) = listener.next().await else { return Ok(()); }; - let remote_address = stream.peer_addr().context("getting peer address")?; + let Ok(remote_address) = stream.peer_addr() else { + // already disconnected + continue; + }; - stream.set_nodelay(true)?; + let _ = stream.set_nodelay(true); if detect_port_knock(&stream).await { continue; } diff --git a/warpgate-protocol-postgres/src/lib.rs b/warpgate-protocol-postgres/src/lib.rs index 19876eb2d..ef7ac425f 100644 --- a/warpgate-protocol-postgres/src/lib.rs +++ b/warpgate-protocol-postgres/src/lib.rs @@ -10,7 +10,7 @@ use std::sync::Arc; use std::time::Duration; use anyhow::{Context, Result}; -use futures::TryStreamExt; +use futures::StreamExt; use rustls::ServerConfig; use rustls::server::NoClientAuth; use session::PostgresSession; @@ -73,7 +73,7 @@ impl ProtocolServer for PostgresProtocolServer { .await .context("accepting connection")?; loop { - let Some(stream) = listener.try_next().await? else { + let Some(stream) = listener.next().await else { return Ok(()); }; @@ -81,19 +81,32 @@ impl ProtocolServer for PostgresProtocolServer { continue; } - let remote_address = stream.peer_addr().context("getting peer address")?; + let Ok(remote_address) = stream.peer_addr() else { + // already disconnected + continue; + }; // Enable TCP keepalive to prevent idle connections from timing out // This is especially important during web auth approval wait // Use socket2 to configure keepalive (tokio TcpStream doesn't expose it directly) - let socket = Socket::from(stream.into_std()?); - let keepalive = TcpKeepalive::new() - .with_time(Duration::from_secs(60)) // Start keepalive after 60s of inactivity - .with_interval(Duration::from_secs(10)) // Send probes every 10s - .with_retries(3); // 3 retries before considering dead - socket.set_tcp_keepalive(&keepalive)?; - socket.set_tcp_nodelay(true)?; - let stream = tokio::net::TcpStream::from_std(socket.into())?; + let stream = (|| { + let socket = Socket::from(stream.into_std()?); + let keepalive = TcpKeepalive::new() + .with_time(Duration::from_secs(60)) // Start keepalive after 60s of inactivity + .with_interval(Duration::from_secs(10)) // Send probes every 10s + .with_retries(3); // 3 retries before considering dead + socket.set_tcp_keepalive(&keepalive)?; + socket.set_tcp_nodelay(true)?; + tokio::net::TcpStream::from_std(socket.into()) + })(); + + let stream = match stream { + Ok(stream) => stream, + Err(error) => { + warn!(%error, "Failed to set up an accepted connection"); + continue; + } + }; let tls_config = tls_config.clone(); let services = self.services.clone(); diff --git a/warpgate-protocol-ssh/src/server/mod.rs b/warpgate-protocol-ssh/src/server/mod.rs index 756a0a12f..dbd011bbd 100644 --- a/warpgate-protocol-ssh/src/server/mod.rs +++ b/warpgate-protocol-ssh/src/server/mod.rs @@ -9,7 +9,7 @@ use std::sync::Arc; use std::time::Duration; use anyhow::{Context, Result}; -use futures::TryStreamExt; +use futures::StreamExt; use russh::keys::{Algorithm, HashAlg, PrivateKey}; use russh::{MethodKind, MethodSet, Preferred}; pub use russh_handler::ServerHandler; @@ -41,7 +41,7 @@ pub async fn run_server(services: Services, address: ListenEndpoint) -> Result<( let mut listener = address.tcp_accept_stream().await?; - while let Some(stream) = listener.try_next().await.context("accepting connection")? { + while let Some(stream) = listener.next().await { let russh_config_init = russh_config_init.clone(); let services = services.clone(); From fd82f777b00e7262cc2b1cd73eb9350edcee88f6 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Thu, 18 Jun 2026 15:25:37 +0200 Subject: [PATCH 196/556] Bump the version-bumps group across 1 directory with 2 updates (#2069) Signed-off-by: dependabot[bot] --- tests/poetry.lock | 340 +++++++++++++++++++++++----------------------- 1 file changed, 170 insertions(+), 170 deletions(-) diff --git a/tests/poetry.lock b/tests/poetry.lock index 01a494702..e481a3e8e 100644 --- a/tests/poetry.lock +++ b/tests/poetry.lock @@ -14,131 +14,131 @@ files = [ [[package]] name = "aiohttp" -version = "3.14.0" +version = "3.14.1" description = "Async http client/server framework (asyncio)" optional = false python-versions = ">=3.10" groups = ["main"] files = [ - {file = "aiohttp-3.14.0-cp310-cp310-macosx_10_9_universal2.whl", hash = "sha256:692e409052e7436029bbb32977cd7c5bf806ac5fa4085b973996785ffadad33c"}, - {file = "aiohttp-3.14.0-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:40af7ebe53c7990e110dc4ad03566b12c3ac996254298a3d39046dd69cfcb2c2"}, - {file = "aiohttp-3.14.0-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:02cb2ffbb7da32f82e21ad9952669c45bd88a80e0878264c2f59fe1c6fb2badd"}, - {file = "aiohttp-3.14.0-cp310-cp310-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:2e2514cb7195f6d7c219339635bea71ae47d1569b051300d32df9dcfabcdb869"}, - {file = "aiohttp-3.14.0-cp310-cp310-manylinux2014_armv7l.manylinux_2_17_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:30e8b7eeb42d02c120ca90d6c6e076a221a16b70a6dac9ae44c7ab5104cc7fe4"}, - {file = "aiohttp-3.14.0-cp310-cp310-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:63e38be0d75a654deaa06be32fb4cab883a4222940be1d05861b6717679cbadb"}, - {file = "aiohttp-3.14.0-cp310-cp310-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:1210d4c87cc00128160c7384ab41877a701295b97cffa6362f908a49b6e8a7ca"}, - {file = "aiohttp-3.14.0-cp310-cp310-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:1a78a77366ed158a0a54b076990e575d7b7cdb728cbfd02711eadab150f2269f"}, - {file = "aiohttp-3.14.0-cp310-cp310-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:f4d2038c64f36df96cfd3fa0937910e231eafbf897e70a06c155a817bb632fa6"}, - {file = "aiohttp-3.14.0-cp310-cp310-musllinux_1_2_aarch64.whl", hash = "sha256:4714c70067a08b604d0bf3bc4dfdf82e52944afab41d0428d460862763d2f79b"}, - {file = "aiohttp-3.14.0-cp310-cp310-musllinux_1_2_armv7l.whl", hash = "sha256:f79bfd2847513a7ac801bbafd1de02348a37926ac439eeb4bfe96fcff4eada15"}, - {file = "aiohttp-3.14.0-cp310-cp310-musllinux_1_2_ppc64le.whl", hash = "sha256:25e9f1d2465a210d60edb64d7b204a147e85d4c194eecef3d1604fb5ace678ce"}, - {file = "aiohttp-3.14.0-cp310-cp310-musllinux_1_2_riscv64.whl", hash = "sha256:b5314743ebe926c2fda35d0a298c565c885505f6635c2a30936363404cf274a7"}, - {file = "aiohttp-3.14.0-cp310-cp310-musllinux_1_2_s390x.whl", hash = "sha256:28eee8de1d69711c53116df8202f1c2aa0e3f80ef912a88fc18d159d53e7110b"}, - {file = "aiohttp-3.14.0-cp310-cp310-musllinux_1_2_x86_64.whl", hash = "sha256:89ed35666c95d3efe1955056afcde09e62a57a34e2a4398b17f9f6c1564f0b25"}, - {file = "aiohttp-3.14.0-cp310-cp310-win32.whl", hash = "sha256:5e4646e9a6af29af354204011bf5769cb0276ec5b64653e42f90b3e13845169f"}, - {file = "aiohttp-3.14.0-cp310-cp310-win_amd64.whl", hash = "sha256:22a8d06f204e0518a586d770032db3c7043c9ba3693081b3e3ad425e1458d594"}, - {file = "aiohttp-3.14.0-cp310-cp310-win_arm64.whl", hash = "sha256:4acfc34bd4d3c58754fc9f22ff1b5e92aabce68f3d4bf7b71a0b732d9bceb78a"}, - {file = "aiohttp-3.14.0-cp311-cp311-macosx_10_9_universal2.whl", hash = "sha256:54bf3522d6f7351e55f89a62d5c2bf138ad557b031670266c5df604ae88e0b5a"}, - {file = "aiohttp-3.14.0-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:0746d9fb0ac4fdef643a84494efe3f06d50335dd8c7a530228b86448aae0a803"}, - {file = "aiohttp-3.14.0-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:9f3a96b6d39a4872222beee72e1df41d2ff886ae96152cf3e757ef8c5673ef0e"}, - {file = "aiohttp-3.14.0-cp311-cp311-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:d336820adbb914debbc90a1d8c1bfc4bea55996aecf64866a989d35d1f9fd903"}, - {file = "aiohttp-3.14.0-cp311-cp311-manylinux2014_armv7l.manylinux_2_17_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:71b2604c9bfc1b115547d63a094d5244b3f02799833513a99a68aaa7b167c4cb"}, - {file = "aiohttp-3.14.0-cp311-cp311-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:610d68800435903e303ca0542b9d3e4eb72a12ff33a6d471a070c1d81eebd3c2"}, - {file = "aiohttp-3.14.0-cp311-cp311-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:514db9a79337068981ee2137310283a07b4b885c584991097a91a4da419bcb81"}, - {file = "aiohttp-3.14.0-cp311-cp311-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:c452d17eeb95d563fc8b936f3050301dbd1d268126c4632d8b70ede9696202ee"}, - {file = "aiohttp-3.14.0-cp311-cp311-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:ed94a81506e3d1bdbad5108f497a58f2a2354aedb4ca314d5326f07d1fd1ac2d"}, - {file = "aiohttp-3.14.0-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:1394dce36e0f0d260ac0b555a654de19cb989f3c1b8bdd24f505314dfea18a00"}, - {file = "aiohttp-3.14.0-cp311-cp311-musllinux_1_2_armv7l.whl", hash = "sha256:d1467d1e7b48a73ca7237e0ee4335f3d02b923dbc27b82fd254bc301c97d4026"}, - {file = "aiohttp-3.14.0-cp311-cp311-musllinux_1_2_ppc64le.whl", hash = "sha256:6a5f3532125233c261cf61f32df4059cfcf482eb793c7d3db8452e3142028b86"}, - {file = "aiohttp-3.14.0-cp311-cp311-musllinux_1_2_riscv64.whl", hash = "sha256:3ea81eb518a2ecb319d8ec6d1424a37c773f6634bd87d6985eb606b2faac419f"}, - {file = "aiohttp-3.14.0-cp311-cp311-musllinux_1_2_s390x.whl", hash = "sha256:32e735c3182de7b64f6941a4ede48b38c7f47d9437bd615dd30b5bda8fa1bc93"}, - {file = "aiohttp-3.14.0-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:c21ca9a1c63d4509158f478aeb9d02914dcc52adc68d1bc9dee2452284ee5996"}, - {file = "aiohttp-3.14.0-cp311-cp311-win32.whl", hash = "sha256:19ca5fc84130675ba11c6ca5c7da5cb65f7bf8a32cdd2b616bf49cd334688aae"}, - {file = "aiohttp-3.14.0-cp311-cp311-win_amd64.whl", hash = "sha256:d488e6e9d3bb8ba5ae7066d5be885ae9670eba021b8c6ccb9a3a568e6b19d6e5"}, - {file = "aiohttp-3.14.0-cp311-cp311-win_arm64.whl", hash = "sha256:8b93618102caf12801638a01a2b478a55410ddd71bd41cfaf6f707953a49ac43"}, - {file = "aiohttp-3.14.0-cp312-cp312-macosx_10_13_universal2.whl", hash = "sha256:b29518c9c2ec7e373e68259206a137c7f4f5439c58baaec4b5ab3ab799850a4e"}, - {file = "aiohttp-3.14.0-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:dbec68ce61b64cb73cab4d33df9433427b1713c8bcccb181dce695c1b6f8e87c"}, - {file = "aiohttp-3.14.0-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:3cdf534aa455593e589302990c5097aa5c92c06c4262a20da22934f9186a5fff"}, - {file = "aiohttp-3.14.0-cp312-cp312-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:cb6c657104393b5fbff01a5f59b2023db74058a8077d94475d6c25d03882a108"}, - {file = "aiohttp-3.14.0-cp312-cp312-manylinux2014_armv7l.manylinux_2_17_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:46fbbec4e4fab7428d4396a3823f9320e4560aa3113b89eeebce712c27c9ed5a"}, - {file = "aiohttp-3.14.0-cp312-cp312-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:2c2c7e05dd5335b298085abf45ddf98673934c3ee1c083d0b9ea13d4186ad500"}, - {file = "aiohttp-3.14.0-cp312-cp312-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:3c7139100fbaae76515b73051d8f0aa3a3ff02e415eec8a8eee8e2223d9ba955"}, - {file = "aiohttp-3.14.0-cp312-cp312-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:78d6f9286a629ce52728430afe18f8ed2b6c39a1fddb3802d7244b9983910ad2"}, - {file = "aiohttp-3.14.0-cp312-cp312-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:cc3c3e12cdaeb92d7dcf13db00e9f6b1956b910e47256e696df1cfa946d02159"}, - {file = "aiohttp-3.14.0-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:4d6a998191f5ebe3b8c28463ff72bc030250008b3193c402464efadd08b5ca02"}, - {file = "aiohttp-3.14.0-cp312-cp312-musllinux_1_2_armv7l.whl", hash = "sha256:0fc2b75ae8d169d853be2862d960be8550da6c5c65711d5476407eb3fdb006bd"}, - {file = "aiohttp-3.14.0-cp312-cp312-musllinux_1_2_ppc64le.whl", hash = "sha256:16eee56bcc72d04600bc56c1759982c2385ec0b41d3fd3521f836bf64a0957ef"}, - {file = "aiohttp-3.14.0-cp312-cp312-musllinux_1_2_riscv64.whl", hash = "sha256:5a2e7ca615c3ddc15b82687e05a624e5f5cba3f1d6c20cb81172d70ea498451e"}, - {file = "aiohttp-3.14.0-cp312-cp312-musllinux_1_2_s390x.whl", hash = "sha256:f0b7b8bbbec3ce9467ee0ebe334622fd90624f593edd3136c567811453fc4fae"}, - {file = "aiohttp-3.14.0-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:5ba10966d4f03dd96a14365be4b8e37c327c76f11c3ca867116966cdd9f98066"}, - {file = "aiohttp-3.14.0-cp312-cp312-win32.whl", hash = "sha256:101df7779c80c0636014a6b2c6642acd3efb5b355d48347c9d7dfb720aee9430"}, - {file = "aiohttp-3.14.0-cp312-cp312-win_amd64.whl", hash = "sha256:b0a5747586d4467efd1f932710b269131c9717a872dce082cd92a00c1c13123a"}, - {file = "aiohttp-3.14.0-cp312-cp312-win_arm64.whl", hash = "sha256:5f1c5be60add78fabb4aacd13c5a348ae79d2fcbfc7fa78da8f1eb192273b370"}, - {file = "aiohttp-3.14.0-cp313-cp313-android_21_arm64_v8a.whl", hash = "sha256:25400d710641a8040bf022a8a99f579e581ffa1c5bd42c33255d7d6f3957c127"}, - {file = "aiohttp-3.14.0-cp313-cp313-android_21_x86_64.whl", hash = "sha256:c5492b9929826e07cc3fcb9739ae87aab05dff6b5e67a9b73fd1700c6d008981"}, - {file = "aiohttp-3.14.0-cp313-cp313-ios_13_0_arm64_iphoneos.whl", hash = "sha256:3366751d68d237c621264233a32f3078bbc21b7904ab90a77e03d21390c742c6"}, - {file = "aiohttp-3.14.0-cp313-cp313-ios_13_0_arm64_iphonesimulator.whl", hash = "sha256:57ea07d28695a7a40304d42251892a8df765e5588c10ee32afeddcd5df33c0a2"}, - {file = "aiohttp-3.14.0-cp313-cp313-ios_13_0_x86_64_iphonesimulator.whl", hash = "sha256:076cb014191ae2e65d949e1ad01f1dcfe33e32789b5172510f3e79c79fc04d50"}, - {file = "aiohttp-3.14.0-cp313-cp313-macosx_10_13_universal2.whl", hash = "sha256:2f3fc37054564dee64a855b5b092d87ec35dcddfaabf7dacb1c8a2b1f83dc0a9"}, - {file = "aiohttp-3.14.0-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:8fcaef74d2ab0f607d7ff85a0d15e21bb5a258c4a58df1908396eb50d7f4ed3c"}, - {file = "aiohttp-3.14.0-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:e4c01b0bfc6209590960e68eac083cd22d5d87c21f974dd6208cafa5d3542bc8"}, - {file = "aiohttp-3.14.0-cp313-cp313-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:f12eb7896e81caf403a2b18c9406426f1207361e7239c057ab29c076d4257e83"}, - {file = "aiohttp-3.14.0-cp313-cp313-manylinux2014_armv7l.manylinux_2_17_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:6c79a044cacf360ec46738d863d2f41c9300d2a06ef4a7402ea0df306a350e61"}, - {file = "aiohttp-3.14.0-cp313-cp313-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:85e0675f47be4eff0636bf88c02140ea89168ae0df3ff1f3f464e9de9610d277"}, - {file = "aiohttp-3.14.0-cp313-cp313-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:7b33e751cab03fdc960095b1e326cb5a03f5ee577d6ded59f3d1c100f8668882"}, - {file = "aiohttp-3.14.0-cp313-cp313-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:26d9224c6dd7f5c749aba4f61315a894601448b28d94d12f4dea0903e26d2096"}, - {file = "aiohttp-3.14.0-cp313-cp313-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:6281aecdf2732940f4fe06bd6adec5ae4d59b78b080b8e3a6b81467301010988"}, - {file = "aiohttp-3.14.0-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:23e8314e7aed8576fbe33314d218bd81447a3adbc91dc36f1163bf583cd3084c"}, - {file = "aiohttp-3.14.0-cp313-cp313-musllinux_1_2_armv7l.whl", hash = "sha256:3b54fbff46127aeafdd764cecd0d99fa2f24a0e37ea5c18a7c3a4ac450df1db3"}, - {file = "aiohttp-3.14.0-cp313-cp313-musllinux_1_2_ppc64le.whl", hash = "sha256:b27d89af91a555f58e08e4902dbcbc48862fd40095720ca705990476bd93b7ac"}, - {file = "aiohttp-3.14.0-cp313-cp313-musllinux_1_2_riscv64.whl", hash = "sha256:25d2326a4967bf705a9f9913a13005e93b6020ad8a9f6bd6bd78850d5171332e"}, - {file = "aiohttp-3.14.0-cp313-cp313-musllinux_1_2_s390x.whl", hash = "sha256:a1d209375c503472b3c0a340cdf3c55fcd82e84b46dda7caeaced59faba373ec"}, - {file = "aiohttp-3.14.0-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:666c7c5036df57b693026398b69b41874a1931ac5b3485fd910e57bfac253869"}, - {file = "aiohttp-3.14.0-cp313-cp313-win32.whl", hash = "sha256:23f094a1ef64823fd35854ddf5c7a80a078162f37f9d2f7c6142b51a6affa456"}, - {file = "aiohttp-3.14.0-cp313-cp313-win_amd64.whl", hash = "sha256:e03abdaa17d553f17e1d1d06bb266b3970106c78051d06795723e748d8e49d11"}, - {file = "aiohttp-3.14.0-cp313-cp313-win_arm64.whl", hash = "sha256:acdb400538cf4769543548bb5d1eb23d39bed4f96554a6078cb728c7cb2c268b"}, - {file = "aiohttp-3.14.0-cp314-cp314-android_24_arm64_v8a.whl", hash = "sha256:363ef9e91014e7891679bfb2ac0a7c6ea93435dbbfd10ecf41b9f06fcf506c5f"}, - {file = "aiohttp-3.14.0-cp314-cp314-android_24_x86_64.whl", hash = "sha256:884a4edbdad77be9d0ef36142c8b504351b170df0bf62b51e784fadabf311c42"}, - {file = "aiohttp-3.14.0-cp314-cp314-ios_13_0_arm64_iphoneos.whl", hash = "sha256:70ea956f6cc4a37620966b56c2e205d88ca3e6d85ec063277e414b1035cddad3"}, - {file = "aiohttp-3.14.0-cp314-cp314-ios_13_0_arm64_iphonesimulator.whl", hash = "sha256:ea3b9806c89f61da22fddf1f12dd524fb368e5e28f1261fbdafe5c3cd8ce893b"}, - {file = "aiohttp-3.14.0-cp314-cp314-ios_13_0_x86_64_iphonesimulator.whl", hash = "sha256:a071be341c2bd9b0188e62d173509f024e0a35b1c342c53c50f8daaeda8c3bd8"}, - {file = "aiohttp-3.14.0-cp314-cp314-macosx_10_15_universal2.whl", hash = "sha256:198cfe61bf253b19da1fb3e0fa122249dc4f14c12709493fed8054aa0411cc76"}, - {file = "aiohttp-3.14.0-cp314-cp314-macosx_10_15_x86_64.whl", hash = "sha256:9dc203d6ce6b9106d54e2a93f41dfdfebfbca2d99962ba503bfd3e5921a6549e"}, - {file = "aiohttp-3.14.0-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:9e19d17ab02bf16832a2c8c0d55a486792c5b1645665652ee9531aebcc30cb72"}, - {file = "aiohttp-3.14.0-cp314-cp314-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:d925fba0c14d5b498a8028b0107beebdfd16c5d48d702ff54f879cb017aaaca3"}, - {file = "aiohttp-3.14.0-cp314-cp314-manylinux2014_armv7l.manylinux_2_17_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:d33e61021222ce7f9792bcac870d6f58d8adfceda33ab857b01264f4560f2c5f"}, - {file = "aiohttp-3.14.0-cp314-cp314-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:44eca38755d0105bb32f47d085f5dd449846a449e1245fc105889e3279dcf8e3"}, - {file = "aiohttp-3.14.0-cp314-cp314-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:f13087e06f68fea4941c21a0c541c00553aa16e4f8fd7bbe2b198df761e964d6"}, - {file = "aiohttp-3.14.0-cp314-cp314-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:ff82be7f1ef73634cb77890a770743239bc3d487b848669be1c599889336dc0a"}, - {file = "aiohttp-3.14.0-cp314-cp314-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:a150c0875ac8fd87f1c398650841308a30d65facf7416b12dbdb9cfdcbe5a48c"}, - {file = "aiohttp-3.14.0-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:edc01ea4e1ec5a1649a28866262bf24195889ff7b27bdd947029a6086741de9b"}, - {file = "aiohttp-3.14.0-cp314-cp314-musllinux_1_2_armv7l.whl", hash = "sha256:540632bf882ff8fc88f2e1697be0761578e89e0d79fb4a8a6d65dc5da7e729d4"}, - {file = "aiohttp-3.14.0-cp314-cp314-musllinux_1_2_ppc64le.whl", hash = "sha256:860a86bc2c80237f5dff52edcf427e10a8d8352271fd84845429a3e60199e02c"}, - {file = "aiohttp-3.14.0-cp314-cp314-musllinux_1_2_riscv64.whl", hash = "sha256:5cbd50e6a50d6b99283a826b18cbdebf65b0797689a7535cb0e9dd37be0f63c3"}, - {file = "aiohttp-3.14.0-cp314-cp314-musllinux_1_2_s390x.whl", hash = "sha256:20144819e99db593e22bbd2f3f2691a5e149f879142d6b8670254708853ff4fb"}, - {file = "aiohttp-3.14.0-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:26b6d79aa54cb4ed50cc7d41ed14e99e0f1fc8e7c2d42f2e05b37aea897b2b52"}, - {file = "aiohttp-3.14.0-cp314-cp314-win32.whl", hash = "sha256:106ed074a856f3e21d186b8579e2c8afb6da598e267cdaab01059e13db2fc44d"}, - {file = "aiohttp-3.14.0-cp314-cp314-win_amd64.whl", hash = "sha256:4f770846edae8f00ecc57af825bce811f787f87a7dcf0e90d191790efe5b31f7"}, - {file = "aiohttp-3.14.0-cp314-cp314-win_arm64.whl", hash = "sha256:acf1581c4f21ed4b80a2dded504d87b055a071a84d5737ea966435f768275ac6"}, - {file = "aiohttp-3.14.0-cp314-cp314t-macosx_10_15_universal2.whl", hash = "sha256:6aa1a40f9cbb3da9f80714c5966b8946c21e6a2530d809b9498b33161e3c8733"}, - {file = "aiohttp-3.14.0-cp314-cp314t-macosx_10_15_x86_64.whl", hash = "sha256:b62af5a8cc96a194eaa01a9ed7b34a3ffa58d3d8daaa1a0d7a749353ad12d228"}, - {file = "aiohttp-3.14.0-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:6eb63b1417efaf7d1002a6ad034a40d44376afcc16508a57f8e74b49ad26a095"}, - {file = "aiohttp-3.14.0-cp314-cp314t-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:c20b9ad156a79eb97be5cf9e069eec01d2f0dc8472ffbd75299a8b2d4c2cbbde"}, - {file = "aiohttp-3.14.0-cp314-cp314t-manylinux2014_armv7l.manylinux_2_17_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:40ae7b0642c25632c7eabc4a04754012691864d2a1b93becf7cddb76027b838a"}, - {file = "aiohttp-3.14.0-cp314-cp314t-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:95f5217e76a046b9f228a101717ef8d42b1eb3d9d196d15202db5bf41df88936"}, - {file = "aiohttp-3.14.0-cp314-cp314t-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:1a4a9f17e85b80878c176695c1998c790e83731d8271881e5d356488652a1f9e"}, - {file = "aiohttp-3.14.0-cp314-cp314t-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:145262119b07d7f95abc1839add35ba2bfc84551d4b4660ca11542c0b215455b"}, - {file = "aiohttp-3.14.0-cp314-cp314t-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:49a33ded29b0b2fa7a367a02cf0fb89af602bb87542a16177ec8ce1c9c51d12a"}, - {file = "aiohttp-3.14.0-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:2cc736a9c9fc2bc4dd71fd404815741b6573df27c3f985948ec4076989ac57de"}, - {file = "aiohttp-3.14.0-cp314-cp314t-musllinux_1_2_armv7l.whl", hash = "sha256:b4141a3e5342ee3053a9cab54d25b64ed28289c1041e4c54b3d99839314d90ce"}, - {file = "aiohttp-3.14.0-cp314-cp314t-musllinux_1_2_ppc64le.whl", hash = "sha256:e30871b2d58996cb81aac52d2b1d15ac05257131ef0f90f18c2115a380fbfe7c"}, - {file = "aiohttp-3.14.0-cp314-cp314t-musllinux_1_2_riscv64.whl", hash = "sha256:667b881d083ccae3900ea5a241e17e5007ca78844c53ed389bb63d48f729d9c7"}, - {file = "aiohttp-3.14.0-cp314-cp314t-musllinux_1_2_s390x.whl", hash = "sha256:b584dfe615d151e9b8f0a8ecb3aee6147f2927ec5b95ba25fe621f5377510928"}, - {file = "aiohttp-3.14.0-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:6199707cc40e0e9cd39c36fbc97bec416c704e1d0ddce03412bb3b3e6a90ccd0"}, - {file = "aiohttp-3.14.0-cp314-cp314t-win32.whl", hash = "sha256:a8d93334d4961c9d566b1f046c81dee475b7c21eb730728d38237bfa70d1c8e6"}, - {file = "aiohttp-3.14.0-cp314-cp314t-win_amd64.whl", hash = "sha256:2d2ffe9b614f50f069068b3b52e73414e4107fc10b7efc939a76acff9251fdd2"}, - {file = "aiohttp-3.14.0-cp314-cp314t-win_arm64.whl", hash = "sha256:7a3fc4358e65826c515350f199c210de747cf669998211b1ee6c2e46de364b24"}, - {file = "aiohttp-3.14.0.tar.gz", hash = "sha256:2882de819734c715fd1b9c11c97e09fa020d14438203d1d354d8ed1702791c9b"}, + {file = "aiohttp-3.14.1-cp310-cp310-macosx_10_9_universal2.whl", hash = "sha256:8f6bb621e5863cfe8fe5ff5468002d200ec31f30f1280b259dc505b02595099e"}, + {file = "aiohttp-3.14.1-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:4f7215cb3933784f79ed20e5f050e15984f390424339b22375d5a53c933a0491"}, + {file = "aiohttp-3.14.1-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:d9d4e294455b23a68c9b8f042d0e8e377a265bcb15332753695f6e5b6819e0ce"}, + {file = "aiohttp-3.14.1-cp310-cp310-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:b238af795833d5731d049d82bc84b768ae6f8f97f0495963b3ed9935c5901cc3"}, + {file = "aiohttp-3.14.1-cp310-cp310-manylinux2014_armv7l.manylinux_2_17_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:e4e5e0ae56914ecdbf446493addefc0159053dd53962cef37d7839f37f73d505"}, + {file = "aiohttp-3.14.1-cp310-cp310-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:092e4ce3619a7c6dee52a6bdabda973d9b34b66781f840ce93c7e0cec30cf521"}, + {file = "aiohttp-3.14.1-cp310-cp310-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:bb33777ea21e8b7ecde0e6fc84f598be0a1192eab1a63bc746d75aa75d38e7bd"}, + {file = "aiohttp-3.14.1-cp310-cp310-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:23119f8fd4f5d16902ed459b63b100bcd269628075162bddac56cc7b5273b3fb"}, + {file = "aiohttp-3.14.1-cp310-cp310-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:57fc6745a4b7d0f5a9eb4f40a69718be6c0bc1b8368cc9fe89e90118719f4f42"}, + {file = "aiohttp-3.14.1-cp310-cp310-musllinux_1_2_aarch64.whl", hash = "sha256:6fd35beba67c4183b09375c5fff9accb47524191a244a99f95fd4472f5402c2b"}, + {file = "aiohttp-3.14.1-cp310-cp310-musllinux_1_2_armv7l.whl", hash = "sha256:672b9d65f42eb877f5c3f234a4547e4e1a226ca8c2eed879bb34670a0ce51192"}, + {file = "aiohttp-3.14.1-cp310-cp310-musllinux_1_2_ppc64le.whl", hash = "sha256:24ba13339fed9251d9b1a1bec8c7ab84c0d1675d79d33501e11f94f8b9a84e05"}, + {file = "aiohttp-3.14.1-cp310-cp310-musllinux_1_2_riscv64.whl", hash = "sha256:94da27378da0610e341c4d30de29a191672683cc82b8f9556e8f7c7212a020fe"}, + {file = "aiohttp-3.14.1-cp310-cp310-musllinux_1_2_s390x.whl", hash = "sha256:52cdac9432d8b4a719f35094a818d95adcae0f0b4fe9b9b921909e0c87de9e7d"}, + {file = "aiohttp-3.14.1-cp310-cp310-musllinux_1_2_x86_64.whl", hash = "sha256:672ac254412a24d0d0cf00a9e6c238877e4be5e5fa2d188832c1244f45f31966"}, + {file = "aiohttp-3.14.1-cp310-cp310-win32.whl", hash = "sha256:2fe3607e71acc6ebb0ec8e492a247bf7a291226192dc0084236dfc12478916f6"}, + {file = "aiohttp-3.14.1-cp310-cp310-win_amd64.whl", hash = "sha256:30099eda75a53c32efb0920e9c33c195314d2cc1c680fbfd30894932ac5f27df"}, + {file = "aiohttp-3.14.1-cp310-cp310-win_arm64.whl", hash = "sha256:5a837f49d901f9e368651b676912bff1104ed8c1a83b280bcd7b29adccef5c9c"}, + {file = "aiohttp-3.14.1-cp311-cp311-macosx_10_9_universal2.whl", hash = "sha256:aa00140699487bd435fde4342d85c94cb256b7cd3a5b9c3396c67f19922afda2"}, + {file = "aiohttp-3.14.1-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:1c1af67559445498b502030c35c59db59966f47041ca9de5b4e707f86bd10b5f"}, + {file = "aiohttp-3.14.1-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:d44ec478e713ee7f29b439f7eb8dc2b9d4079e11ae114d2c2ac3d5daf30516c8"}, + {file = "aiohttp-3.14.1-cp311-cp311-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:d3b1a184a9a8f548a6b73f1e26b96b052193e4b3175ed7342aaf1151a1f00a04"}, + {file = "aiohttp-3.14.1-cp311-cp311-manylinux2014_armv7l.manylinux_2_17_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:5f2504bc0322437c9a1ff6d3333ca56c7477b727c995f036b976ae17b98372c8"}, + {file = "aiohttp-3.14.1-cp311-cp311-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:73f05ea02013e02512c3bf42714f1208c57168c779cc6fe23516e4543089d0a6"}, + {file = "aiohttp-3.14.1-cp311-cp311-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:797457503c2d426bee06eef808d07b31ede30b65e054444e7de64cad0061b7af"}, + {file = "aiohttp-3.14.1-cp311-cp311-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:b821a1f7dedf7e37450654e620038ac3b2e81e8fa6ea269337e97101978ec730"}, + {file = "aiohttp-3.14.1-cp311-cp311-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:4cd96b5ba05d67ed0cf00b5b405c8cd99586d8e3481e8ee0a831057591af7621"}, + {file = "aiohttp-3.14.1-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:1d459b98a932296c6f0e94f87511a0b1b90a8a02c30a50e60a297619cd5a58ee"}, + {file = "aiohttp-3.14.1-cp311-cp311-musllinux_1_2_armv7l.whl", hash = "sha256:764457a7be60825fb770a644852ff717bcbb5042f189f2bd16df61a81b3f6573"}, + {file = "aiohttp-3.14.1-cp311-cp311-musllinux_1_2_ppc64le.whl", hash = "sha256:f7a16ef45b081454ef844502d87a848876c490c4cb5c650c230f6ec79ed2c1e7"}, + {file = "aiohttp-3.14.1-cp311-cp311-musllinux_1_2_riscv64.whl", hash = "sha256:2fbc3ed048b3475b9f0cbcb9978e9d2d3511acd91ead203af26ed9f0056004cf"}, + {file = "aiohttp-3.14.1-cp311-cp311-musllinux_1_2_s390x.whl", hash = "sha256:bedb0cd073cc2dc035e30aeb99444389d3cd2113afe4ef9fcd23d439f5bade85"}, + {file = "aiohttp-3.14.1-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:b6feea921016eb3d4e04d65fc4e9ca402d1a3801f562aef94989f54694917af3"}, + {file = "aiohttp-3.14.1-cp311-cp311-win32.whl", hash = "sha256:313701e488100074ce99850404ee36e741abf6330179fec908a1944ecf570126"}, + {file = "aiohttp-3.14.1-cp311-cp311-win_amd64.whl", hash = "sha256:03ab4530fdcb3a543a122ba4b65ac9919da9fe9f78a03d328a6e38ff962f7aa5"}, + {file = "aiohttp-3.14.1-cp311-cp311-win_arm64.whl", hash = "sha256:486f7d16ed54c39c2cbd7ca71fd8ba2b8bb7860df65bd7b6ed640bab96a38a8b"}, + {file = "aiohttp-3.14.1-cp312-cp312-macosx_10_13_universal2.whl", hash = "sha256:d35143e27778b4bb0fb189562d7f275bff79c62ab8e98459717c0ea617ff2480"}, + {file = "aiohttp-3.14.1-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:bcfb80a2cc36fba2534e5e5b5264dc7ae6fcd9bf15256da3e53d2f499e6fa29d"}, + {file = "aiohttp-3.14.1-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:27fd7c91e51729b4f7e1577865fa6d34c9adccbc39aabe9000285b48af9f0ec2"}, + {file = "aiohttp-3.14.1-cp312-cp312-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:64c567bf9eaf664280116a8688f63016e6b32db2505908e2bdaca1b6438142f2"}, + {file = "aiohttp-3.14.1-cp312-cp312-manylinux2014_armv7l.manylinux_2_17_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:f5e6ff2bdbb8f4cd3fbe41f99e25bbcd58e3bf9f13d3dd31a11e7917251cc77a"}, + {file = "aiohttp-3.14.1-cp312-cp312-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:2f73e01dc37122325caf079982621262f96d74823c179038a82fddfc50359264"}, + {file = "aiohttp-3.14.1-cp312-cp312-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:bb2c0c80d431c0d03f2c7dbf125150fedd4f0de17366a7ca33f7ccb822391842"}, + {file = "aiohttp-3.14.1-cp312-cp312-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:3e6fc1a85fa7194a1a7d19f44e8609180f4a8eb5fa4c7ed8b4355f080fad235c"}, + {file = "aiohttp-3.14.1-cp312-cp312-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:686b6c0d3911ec387b444ddf5dc62fb7f7c0a7d5186a7861626496a5ab4aff95"}, + {file = "aiohttp-3.14.1-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:c6fa4dc7ad6f8109c70bb1499e589f76b0b792baf39f9b017eb92c8a81d0a199"}, + {file = "aiohttp-3.14.1-cp312-cp312-musllinux_1_2_armv7l.whl", hash = "sha256:87a5eea1b2a5e21e1ebdbb33ad4165359189327e63fc4e4894693e7f821ac817"}, + {file = "aiohttp-3.14.1-cp312-cp312-musllinux_1_2_ppc64le.whl", hash = "sha256:1c1421eb01d4fd608d88cc8290211d177a58532b55ad94076fb349c5bf467f0a"}, + {file = "aiohttp-3.14.1-cp312-cp312-musllinux_1_2_riscv64.whl", hash = "sha256:34b257ec41345c1e8f2df68fa908a7952f5de932723871eb633ecbbff396c9a4"}, + {file = "aiohttp-3.14.1-cp312-cp312-musllinux_1_2_s390x.whl", hash = "sha256:de538791a80e5d862addbc183f70f0158ac9b9bb872bb147f1fd2a683691e087"}, + {file = "aiohttp-3.14.1-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:6f71173be42d3241d428f760122febb748de0623f44308a6f120d0dd9ec572e3"}, + {file = "aiohttp-3.14.1-cp312-cp312-win32.whl", hash = "sha256:ec8dc383ee57ea3e883477dcca3f11b65d58199f1080acaf4cd6ad9a99698be4"}, + {file = "aiohttp-3.14.1-cp312-cp312-win_amd64.whl", hash = "sha256:2aa92c87868cd13674989f9ee83e5f9f7ea4237589b728048e1f0c8f6caa3271"}, + {file = "aiohttp-3.14.1-cp312-cp312-win_arm64.whl", hash = "sha256:2c840c90759922cb5e6dda94596e079a30fb5a5ba548e7e0dc00574703940847"}, + {file = "aiohttp-3.14.1-cp313-cp313-android_21_arm64_v8a.whl", hash = "sha256:b3a03285a7f9c7b016324574a6d92a1c895da6b978cb8f1deee3ac72bc6da178"}, + {file = "aiohttp-3.14.1-cp313-cp313-android_21_x86_64.whl", hash = "sha256:2a73f487ab8ef5abbb24b7aa9b73e98eaba9e9e031804ff2416f02eca315ccaf"}, + {file = "aiohttp-3.14.1-cp313-cp313-ios_13_0_arm64_iphoneos.whl", hash = "sha256:915fbb7b41b115192259f8c9ae58f3ddc444d2b5579917270211858e606a4afd"}, + {file = "aiohttp-3.14.1-cp313-cp313-ios_13_0_arm64_iphonesimulator.whl", hash = "sha256:7fb4bdf95b0561a79f259f9d28fbc109728c5ee7f27aff6391f0ca703a329abe"}, + {file = "aiohttp-3.14.1-cp313-cp313-ios_13_0_x86_64_iphonesimulator.whl", hash = "sha256:1b9748363260121d2927704f5d4fc498150669ca3ae93625986ee89c8f80dcd4"}, + {file = "aiohttp-3.14.1-cp313-cp313-macosx_10_13_universal2.whl", hash = "sha256:86a6dab78b0e43e2897a3bbe15745aa60dc5423ca437b7b0b164c069bf91b876"}, + {file = "aiohttp-3.14.1-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:4dfd6e47d3c44c2279907607f73a4240b88c69eb8b90da7e2441a8045dfd21da"}, + {file = "aiohttp-3.14.1-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:317acd9f8602858dc7d59679812c376c7f0b97bcbbf16e0d6237f54141d8a8a6"}, + {file = "aiohttp-3.14.1-cp313-cp313-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:bd869c427324e5cb15195793de951295710db28be7d818247f3097b4ab5d4b96"}, + {file = "aiohttp-3.14.1-cp313-cp313-manylinux2014_armv7l.manylinux_2_17_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:93b032b5ec3255473c143627d21a69ac74ae12f7f33974cb587c564d11b1066f"}, + {file = "aiohttp-3.14.1-cp313-cp313-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:f234b4deb12f3ad59127e037bc57c40c21e45b45282df7d3a55a0f409f595296"}, + {file = "aiohttp-3.14.1-cp313-cp313-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:9af6779bfb46abf124068327abcdf9ce95c9ef8287a3e8da76ccf2d0f16c28fa"}, + {file = "aiohttp-3.14.1-cp313-cp313-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:faccab372e66bc76d5731525e7f1143c922271725b9d38c9f97edcc66266b451"}, + {file = "aiohttp-3.14.1-cp313-cp313-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:f380468b09d2a81633ee863b0ec5648d364bd17bb8ecfb8c2f387f7ac1faf42c"}, + {file = "aiohttp-3.14.1-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:97e704dcd26271f5bda3fa07c3ce0fb76d6d3f8659f4baa1a24442cc9ba177ca"}, + {file = "aiohttp-3.14.1-cp313-cp313-musllinux_1_2_armv7l.whl", hash = "sha256:269b76ac5394092b95bc4a098f4fc6c191c083c3bd12775d1e30e663132f6a09"}, + {file = "aiohttp-3.14.1-cp313-cp313-musllinux_1_2_ppc64le.whl", hash = "sha256:5c0b3e614340c889d575451696374c9d17affd54cd607ca0babed8f8c37b9397"}, + {file = "aiohttp-3.14.1-cp313-cp313-musllinux_1_2_riscv64.whl", hash = "sha256:5663ee9257cfa1add7253a7da3035a02f31b6600ec48261585e1800a81533080"}, + {file = "aiohttp-3.14.1-cp313-cp313-musllinux_1_2_s390x.whl", hash = "sha256:603a2c834142172ffddc054067f5ec0ca65d57a0aa98a71bc81952573208e345"}, + {file = "aiohttp-3.14.1-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:cb21957bb8aca671c1765e32f58164cf0c50e6bf41c0bbbd16da20732ecaf588"}, + {file = "aiohttp-3.14.1-cp313-cp313-win32.whl", hash = "sha256:e509a55f681e6158c20f70f102f9cf61fb20fbc382272bc6d94b7343f2582780"}, + {file = "aiohttp-3.14.1-cp313-cp313-win_amd64.whl", hash = "sha256:1ac8531b638959718e18c2207fbfe297819875da46a740b29dfa29beba64355a"}, + {file = "aiohttp-3.14.1-cp313-cp313-win_arm64.whl", hash = "sha256:250d14af67f6b6a1a4a811049b1afa69d61d617fca6bf33149b3ab1a6dbcf7b8"}, + {file = "aiohttp-3.14.1-cp314-cp314-android_24_arm64_v8a.whl", hash = "sha256:7c106c26852ca1c2047c6b80384f17100b4e439af276f21ef3d4e2f450ae7e15"}, + {file = "aiohttp-3.14.1-cp314-cp314-android_24_x86_64.whl", hash = "sha256:20205f7f5ade7aaec9f4b500549bbc071b046453aed72f9c06dcab87896a83e8"}, + {file = "aiohttp-3.14.1-cp314-cp314-ios_13_0_arm64_iphoneos.whl", hash = "sha256:62a759436b29e677181a9e76bab8b8f689a29cb9c535f45f7c48c9c830d3f8c3"}, + {file = "aiohttp-3.14.1-cp314-cp314-ios_13_0_arm64_iphonesimulator.whl", hash = "sha256:2964cbf553df4d7a57348da44d961d871895fc1ee4e8c322b2a95612c7b17fba"}, + {file = "aiohttp-3.14.1-cp314-cp314-ios_13_0_x86_64_iphonesimulator.whl", hash = "sha256:237651caadc3a59badd39319c54642b5299e9cc98a3a194310e55d5bb9f5e397"}, + {file = "aiohttp-3.14.1-cp314-cp314-macosx_10_15_universal2.whl", hash = "sha256:896e12dfdbbab9d8f7e16d2b28c6769a60126fa92095d1ebf9473d02593a2448"}, + {file = "aiohttp-3.14.1-cp314-cp314-macosx_10_15_x86_64.whl", hash = "sha256:d03f281ed22579314ba00821ce20115a7c0ac430660b4cc05704a3f818b3e004"}, + {file = "aiohttp-3.14.1-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:07eabb979d236335fed927e137a928c9adfb7df3b9ec7aa31726f133a62be983"}, + {file = "aiohttp-3.14.1-cp314-cp314-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:4fe1f1087cbadb280b5e1bb054a4f00d1423c74d6626c5e48400d871d34ecefe"}, + {file = "aiohttp-3.14.1-cp314-cp314-manylinux2014_armv7l.manylinux_2_17_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:367a9314fdc79dab0fac96e216cb41dd73c85bdca85306ce8999118ba7e0f333"}, + {file = "aiohttp-3.14.1-cp314-cp314-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:a24f677ebe83749039e7bdf862ff0bbb16818ae4193d4ef96505e269375bcce0"}, + {file = "aiohttp-3.14.1-cp314-cp314-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:c83afe0ba876be7e943d2e0ba645809ad441575d2840c895c21ee5de93b9377a"}, + {file = "aiohttp-3.14.1-cp314-cp314-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:634e385930fb6d2d479cf3aa66515955863b77a5e3c2b5894ca259a25b308602"}, + {file = "aiohttp-3.14.1-cp314-cp314-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:eeea07c4397bbc57719c4eed8f9c284874d4f175f9b6d57f7a1546b976d455ca"}, + {file = "aiohttp-3.14.1-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:335c0cc3e3545ce98dcb9cfcb836f40c3411f43fa03dab757597d80c89af8a35"}, + {file = "aiohttp-3.14.1-cp314-cp314-musllinux_1_2_armv7l.whl", hash = "sha256:ae6be797afdef264e8a84864a85b196ca06045586481b3df8a967322fd2fa844"}, + {file = "aiohttp-3.14.1-cp314-cp314-musllinux_1_2_ppc64le.whl", hash = "sha256:8560b4d712474335d08907db7973f71912d3a9a8f1dee992ec06b5d2fe359496"}, + {file = "aiohttp-3.14.1-cp314-cp314-musllinux_1_2_riscv64.whl", hash = "sha256:2b7edd08e0a5deb1e8564a2fcd8f4561014a3f05252334671bbf55ddd47db0e5"}, + {file = "aiohttp-3.14.1-cp314-cp314-musllinux_1_2_s390x.whl", hash = "sha256:b6ff7fcee63287ae57b5df3e4f5957ce032122802509246dec1a5bcc55904c95"}, + {file = "aiohttp-3.14.1-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:6ffbb2f4ec1ceaff7e07d43922954da26b223d188bf30658e561b98e23089444"}, + {file = "aiohttp-3.14.1-cp314-cp314-win32.whl", hash = "sha256:a9875b46d910cff3ea2f5962f9d266b465459fe634e22556ab9bd6fc1192eea0"}, + {file = "aiohttp-3.14.1-cp314-cp314-win_amd64.whl", hash = "sha256:af8b4b81a960eeaf1234971ac3cd0ba5901f3cd42eae42a46b4d089a8b492719"}, + {file = "aiohttp-3.14.1-cp314-cp314-win_arm64.whl", hash = "sha256:cf4491381b1b57425c315a56a439251b1bdac07b2275f19a8c44bc57744532ec"}, + {file = "aiohttp-3.14.1-cp314-cp314t-macosx_10_15_universal2.whl", hash = "sha256:819c054312f1af92947e6a55883d1b66feefab11531a7fc45e0fb9b63880b5c2"}, + {file = "aiohttp-3.14.1-cp314-cp314t-macosx_10_15_x86_64.whl", hash = "sha256:10ee9c1753a8f706345b22496c79fbddb5be0599e0823f3738b1534058e25340"}, + {file = "aiohttp-3.14.1-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:1601cc37baf5750ccacae618ec2daf020769581695550e3b654a911f859c563d"}, + {file = "aiohttp-3.14.1-cp314-cp314t-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:4d6e0ac9da31c9c04c84e1c0182ad8d6df35965a85cae29cd71d089621b3ae94"}, + {file = "aiohttp-3.14.1-cp314-cp314t-manylinux2014_armv7l.manylinux_2_17_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:9e8f2d660c350b3d0e259c7a7e3d9b7fc8b41210cbcc3d4a7076ff0a5e5c2fdc"}, + {file = "aiohttp-3.14.1-cp314-cp314t-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:4691802dda97be727f79d86818acaad7eb8e9252626a1d6b519fedbb92d5e251"}, + {file = "aiohttp-3.14.1-cp314-cp314t-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:c389c482a7e9b9dc3ee2701ac46c4125297a3818875b9c305ddb603c04828fd1"}, + {file = "aiohttp-3.14.1-cp314-cp314t-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:fc0cacab7ba4e56f0f81c82a98c09bed2f39c940107b03a34b168bdf7597edd3"}, + {file = "aiohttp-3.14.1-cp314-cp314t-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:979ed4717f59b8bb12e3963378fa285d93d367e15bcd66c721311826d3c44a6c"}, + {file = "aiohttp-3.14.1-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:38e1e7daaea81df51c952e18483f323d878499a1e2bfe564790e0f9701d6f203"}, + {file = "aiohttp-3.14.1-cp314-cp314t-musllinux_1_2_armv7l.whl", hash = "sha256:4132e72c608fe9fecb8f409113567605915b83e9bdd3ea56538d2f9cd35002f1"}, + {file = "aiohttp-3.14.1-cp314-cp314t-musllinux_1_2_ppc64le.whl", hash = "sha256:eefd9cc9b6d4a2db5f00a26bc3e4f9acf71926a6ec557cd56c9c6f27c290b665"}, + {file = "aiohttp-3.14.1-cp314-cp314t-musllinux_1_2_riscv64.whl", hash = "sha256:b165790117eea512d7f3fb22f1f6dad3d55a7189571993eb015591c1401276d1"}, + {file = "aiohttp-3.14.1-cp314-cp314t-musllinux_1_2_s390x.whl", hash = "sha256:ed09c7eb1c391271c2ed0314a51903e72a3acb653d5ccfc264cdf3ef11f8269d"}, + {file = "aiohttp-3.14.1-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:99abd37084b82f5830c635fddd0b4993b9742a66eb746dacf433c8590e8f9e3c"}, + {file = "aiohttp-3.14.1-cp314-cp314t-win32.whl", hash = "sha256:47ddf841cdecc810749921d25606dee45857d12d2ad5ddb7b5bd7eab12e4b365"}, + {file = "aiohttp-3.14.1-cp314-cp314t-win_amd64.whl", hash = "sha256:5e78b522b7a6e27e0b25d19b247b75039ac4c94f99823e3c9e53ae1603a9f7e9"}, + {file = "aiohttp-3.14.1-cp314-cp314t-win_arm64.whl", hash = "sha256:90d53f1609c29ccc2193945ef732428382a28f78d0456ae4d3daf0d48b74f0f6"}, + {file = "aiohttp-3.14.1.tar.gz", hash = "sha256:307f2cff90a764d329e77040603fa032db89c5c24fdad50c4c15334cba744035"}, ] [package.dependencies] @@ -642,61 +642,61 @@ files = [ [[package]] name = "cryptography" -version = "48.0.0" +version = "48.0.1" description = "cryptography is a package which provides cryptographic recipes and primitives to Python developers." optional = false python-versions = "!=3.9.0,!=3.9.1,>=3.9" groups = ["main"] files = [ - {file = "cryptography-48.0.0-cp311-abi3-macosx_10_9_universal2.whl", hash = "sha256:0c558d2cdffd8f4bbb30fc7134c74d2ca9a476f830bb053074498fbc86f41ed6"}, - {file = "cryptography-48.0.0-cp311-abi3-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:f5333311663ea94f75dd408665686aaf426563556bb5283554a3539177e03b8c"}, - {file = "cryptography-48.0.0-cp311-abi3-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:7995ef305d7165c3f11ae07f2517e5a4f1d5c18da1376a0a9ed496336b69e5f3"}, - {file = "cryptography-48.0.0-cp311-abi3-manylinux_2_28_aarch64.whl", hash = "sha256:40ba1f85eaa6959837b1d51c9767e230e14612eea4ef110ee8854ada22da1bf5"}, - {file = "cryptography-48.0.0-cp311-abi3-manylinux_2_28_ppc64le.whl", hash = "sha256:369a6348999f94bbd53435c894377b20ab95f25a9065c283570e70150d8abc3c"}, - {file = "cryptography-48.0.0-cp311-abi3-manylinux_2_28_x86_64.whl", hash = "sha256:a0e692c683f4df67815a2d258b324e66f4738bd7a96a218c826dce4f4bd05d8f"}, - {file = "cryptography-48.0.0-cp311-abi3-manylinux_2_31_armv7l.whl", hash = "sha256:18349bbc56f4743c8b12dc32e2bccb2cf83ee8b69a3bba74ef8ae857e26b3d25"}, - {file = "cryptography-48.0.0-cp311-abi3-manylinux_2_34_aarch64.whl", hash = "sha256:7e8eac43dfca5c4cccc6dad9a80504436fca53bb9bc3100a2386d730fbe6b602"}, - {file = "cryptography-48.0.0-cp311-abi3-manylinux_2_34_ppc64le.whl", hash = "sha256:9ccdac7d40688ecb5a3b4a604b8a88c8002e3442d6c60aead1db2a89a041560c"}, - {file = "cryptography-48.0.0-cp311-abi3-manylinux_2_34_x86_64.whl", hash = "sha256:bd72e68b06bb1e96913f97dd4901119bc17f39d4586a5adf2d3e47bc2b9d58b5"}, - {file = "cryptography-48.0.0-cp311-abi3-musllinux_1_2_aarch64.whl", hash = "sha256:59baa2cb386c4f0b9905bd6eb4c2a79a69a128408fd31d32ca4d7102d4156321"}, - {file = "cryptography-48.0.0-cp311-abi3-musllinux_1_2_x86_64.whl", hash = "sha256:9249e3cd978541d665967ac2cb2787fd6a62bddf1e75b3e347a594d7dacf4f74"}, - {file = "cryptography-48.0.0-cp311-abi3-win32.whl", hash = "sha256:9c459db21422be75e2809370b829a87eb37f74cd785fc4aa9ea1e5f43b47cda4"}, - {file = "cryptography-48.0.0-cp311-abi3-win_amd64.whl", hash = "sha256:5b012212e08b8dd5edc78ef54da83dd9892fd9105323b3993eff6bea65dc21d7"}, - {file = "cryptography-48.0.0-cp314-cp314t-macosx_10_9_universal2.whl", hash = "sha256:3cb07a3ed6431663cd321ea8a000a1314c74211f823e4177fefa2255e057d1ec"}, - {file = "cryptography-48.0.0-cp314-cp314t-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:8c7378637d7d88016fa6791c159f698b3d3eed28ebf844ac36b9dc04a14dae18"}, - {file = "cryptography-48.0.0-cp314-cp314t-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:cc90c0b39b2e3c65ef52c804b72e3c58f8a04ab2a1871272798e5f9572c17d20"}, - {file = "cryptography-48.0.0-cp314-cp314t-manylinux_2_28_aarch64.whl", hash = "sha256:76341972e1eff8b4bea859f09c0d3e64b96ce931b084f9b9b7db8ef364c30eff"}, - {file = "cryptography-48.0.0-cp314-cp314t-manylinux_2_28_ppc64le.whl", hash = "sha256:55b7718303bf06a5753dcdccf2f3945cf18ad7bffde41b61226e4db31ab89a9c"}, - {file = "cryptography-48.0.0-cp314-cp314t-manylinux_2_28_x86_64.whl", hash = "sha256:a64697c641c7b1b2178e573cbc31c7c6684cd56883a478d75143dbb7118036db"}, - {file = "cryptography-48.0.0-cp314-cp314t-manylinux_2_31_armv7l.whl", hash = "sha256:561215ea3879cb1cbbf272867e2efda62476f240fb58c64de6b393ae19246741"}, - {file = "cryptography-48.0.0-cp314-cp314t-manylinux_2_34_aarch64.whl", hash = "sha256:ad64688338ed4bc1a6618076ba75fd7194a5f1797ac60b47afe926285adb3166"}, - {file = "cryptography-48.0.0-cp314-cp314t-manylinux_2_34_ppc64le.whl", hash = "sha256:906cbf0670286c6e0044156bc7d4af9cbb0ef6db9f73e52c3ec56ba6bdde5336"}, - {file = "cryptography-48.0.0-cp314-cp314t-manylinux_2_34_x86_64.whl", hash = "sha256:ea8990436d914540a40ab24b6a77c0969695ed52f4a4874c5137ccf7045a7057"}, - {file = "cryptography-48.0.0-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:c18684a7f0cc9a3cb60328f496b8e3372def7c5d2df39ac267878b05565aaaae"}, - {file = "cryptography-48.0.0-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:9be5aafa5736574f8f15f262adc81b2a9869e2cfe9014d52a44633905b40d52c"}, - {file = "cryptography-48.0.0-cp314-cp314t-win32.whl", hash = "sha256:c17dfe85494deaeddc5ce251aebd1d60bbe6afc8b62071bb0b469431a000124f"}, - {file = "cryptography-48.0.0-cp314-cp314t-win_amd64.whl", hash = "sha256:27241b1dc9962e056062a8eef1991d02c3a24569c95975bd2322a8a52c6e5e12"}, - {file = "cryptography-48.0.0-cp39-abi3-macosx_10_9_universal2.whl", hash = "sha256:58d00498e8933e4a194f3076aee1b4a97dfec1a6da444535755822fe5d8b0b86"}, - {file = "cryptography-48.0.0-cp39-abi3-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:614d0949f4790582d2cc25553abd09dd723025f0c0e7c67376a1d77196743d6e"}, - {file = "cryptography-48.0.0-cp39-abi3-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:7ce4bfae76319a532a2dc68f82cc32f5676ee792a983187dac07183690e5c66f"}, - {file = "cryptography-48.0.0-cp39-abi3-manylinux_2_28_aarch64.whl", hash = "sha256:2eb992bbd4661238c5a397594c83f5b4dc2bc5b848c365c8f991b6780efcc5c7"}, - {file = "cryptography-48.0.0-cp39-abi3-manylinux_2_28_ppc64le.whl", hash = "sha256:22a5cb272895dce158b2cacdfdc3debd299019659f42947dbdac6f32d68fe832"}, - {file = "cryptography-48.0.0-cp39-abi3-manylinux_2_28_x86_64.whl", hash = "sha256:2b4d59804e8408e2fea7d1fbaf218e5ec984325221db76e6a241a9abd6cdd95c"}, - {file = "cryptography-48.0.0-cp39-abi3-manylinux_2_31_armv7l.whl", hash = "sha256:984a20b0f62a26f48a3396c72e4bc34c66e356d356bf370053066b3b6d54634a"}, - {file = "cryptography-48.0.0-cp39-abi3-manylinux_2_34_aarch64.whl", hash = "sha256:5a5ed8fde7a1d09376ca0b40e68cd59c69fe23b1f9768bd5824f54681626032a"}, - {file = "cryptography-48.0.0-cp39-abi3-manylinux_2_34_ppc64le.whl", hash = "sha256:8cd666227ef7af430aa5914a9910e0ddd703e75f039cef0825cd0da71b6b711a"}, - {file = "cryptography-48.0.0-cp39-abi3-manylinux_2_34_x86_64.whl", hash = "sha256:9071196d81abc88b3516ac8cdfad32e2b66dd4a5393a8e68a961e9161ddc6239"}, - {file = "cryptography-48.0.0-cp39-abi3-musllinux_1_2_aarch64.whl", hash = "sha256:1e2d54c8be6152856a36f0882ab231e70f8ec7f14e93cf87db8a2ed056bf160c"}, - {file = "cryptography-48.0.0-cp39-abi3-musllinux_1_2_x86_64.whl", hash = "sha256:a5da777e32ffed6f85a7b2b3f7c5cbc88c146bfcd0a1d7baf5fcc6c52ee35dd4"}, - {file = "cryptography-48.0.0-cp39-abi3-win32.whl", hash = "sha256:77a2ccbbe917f6710e05ba9adaa25fb5075620bf3ea6fb751997875aff4ae4bd"}, - {file = "cryptography-48.0.0-cp39-abi3-win_amd64.whl", hash = "sha256:16cd65b9330583e4619939b3a3843eec1e6e789744bb01e7c7e2e62e33c239c8"}, - {file = "cryptography-48.0.0-pp311-pypy311_pp73-macosx_11_0_arm64.whl", hash = "sha256:84cf79f0dc8b36ac5da873481716e87aef31fcfa0444f9e1d8b4b2cece142855"}, - {file = "cryptography-48.0.0-pp311-pypy311_pp73-manylinux_2_28_aarch64.whl", hash = "sha256:fdfef35d751d510fcef5252703621574364fec16418c4a1e5e1055248401054b"}, - {file = "cryptography-48.0.0-pp311-pypy311_pp73-manylinux_2_28_x86_64.whl", hash = "sha256:0890f502ddf7d9c6426129c3f49f5c0a39278ed7cd6322c8755ffca6ee675a13"}, - {file = "cryptography-48.0.0-pp311-pypy311_pp73-manylinux_2_34_aarch64.whl", hash = "sha256:ecde28a596bead48b0cfd2a1b4416c3d43074c2d785e3a398d7ec1fc4d0f7fbb"}, - {file = "cryptography-48.0.0-pp311-pypy311_pp73-manylinux_2_34_x86_64.whl", hash = "sha256:4defde8685ae324a9eb9d818717e93b4638ef67070ac9bc15b8ca85f63048355"}, - {file = "cryptography-48.0.0-pp311-pypy311_pp73-win_amd64.whl", hash = "sha256:db63bf618e5dea46c07de12e900fe1cdd2541e6dc9dbae772a70b7d4d4765f6a"}, - {file = "cryptography-48.0.0.tar.gz", hash = "sha256:5c3932f4436d1cccb036cb0eaef46e6e2db91035166f1ad6505c3c9d5a635920"}, + {file = "cryptography-48.0.1-cp311-abi3-macosx_10_9_universal2.whl", hash = "sha256:3e4a1a3232eef2e6c732827d5722db29a0cc8b27af2a4d865b094cf954be9ca1"}, + {file = "cryptography-48.0.1-cp311-abi3-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:32143b24adb918f078134e1e230f1eb8cc04886b92c28b5f0041aaf3e5699225"}, + {file = "cryptography-48.0.1-cp311-abi3-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:f0d27a5696721ef7a672b8c810f6aded391058e0b9486e63e6d93baf765da691"}, + {file = "cryptography-48.0.1-cp311-abi3-manylinux_2_28_aarch64.whl", hash = "sha256:eb86ce1af36fe65041b6db9a8bb064ee621a7e5fded0f80d475ec243477cd242"}, + {file = "cryptography-48.0.1-cp311-abi3-manylinux_2_28_ppc64le.whl", hash = "sha256:b024e784ad6c077ee0147b35ea9cbfc1e34e1fd4c1dcca214c2794d73a12df08"}, + {file = "cryptography-48.0.1-cp311-abi3-manylinux_2_28_x86_64.whl", hash = "sha256:3752f2dbc8f07a30aad2932c986cea495b03bb554887828225da104f732852b6"}, + {file = "cryptography-48.0.1-cp311-abi3-manylinux_2_31_armv7l.whl", hash = "sha256:bd81490cd5801d755cf97bb68ac191f14b708470b1c7cf4580f669b9c9264cd8"}, + {file = "cryptography-48.0.1-cp311-abi3-manylinux_2_34_aarch64.whl", hash = "sha256:66fd0771e7b9c6dcd44cf1120690d2338d16d72795cf40cae2786a39eba65429"}, + {file = "cryptography-48.0.1-cp311-abi3-manylinux_2_34_ppc64le.whl", hash = "sha256:3fd2ca57062b241c856670b073487d2e86c4637937ca5601e48f97bf8e11fc8f"}, + {file = "cryptography-48.0.1-cp311-abi3-manylinux_2_34_x86_64.whl", hash = "sha256:0ee6ea481db1ab889cba043ec1eda17bb9c1ea79db6722f779c3667f9f70322f"}, + {file = "cryptography-48.0.1-cp311-abi3-musllinux_1_2_aarch64.whl", hash = "sha256:f2ceef93cb096aa3c4cc4b5c94ca6131f9196d28c64d6111533402a9b2054d41"}, + {file = "cryptography-48.0.1-cp311-abi3-musllinux_1_2_x86_64.whl", hash = "sha256:9bd3f92d76217892b15df84ca256c2c113d386fdda7a7d8691aeeced976507c6"}, + {file = "cryptography-48.0.1-cp311-abi3-win32.whl", hash = "sha256:b9a32b876490d66c8bcc9963ef220199569748434ab01a9d6aaeabf88e7f5158"}, + {file = "cryptography-48.0.1-cp311-abi3-win_amd64.whl", hash = "sha256:39489bfca54c7a1f6b297efcd8bc608ab92d16c4ca631b0cad4da46724588b24"}, + {file = "cryptography-48.0.1-cp314-cp314t-macosx_10_9_universal2.whl", hash = "sha256:f817adc181390bd54f2f700107a7419040fb7c1bdf2fc26f36551a06a68c3345"}, + {file = "cryptography-48.0.1-cp314-cp314t-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:d5d30989c6917b478b5817902e85fddaea2261efa8648383d965381ccb9e1ac4"}, + {file = "cryptography-48.0.1-cp314-cp314t-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:df637c05205ea7c1d7fbcbe54bbfea648a52951155f997af13d895d0ecc96991"}, + {file = "cryptography-48.0.1-cp314-cp314t-manylinux_2_28_aarch64.whl", hash = "sha256:869c3b8a53bfe27147832df48b32adadf558249d50e76cb3769d40e986b13265"}, + {file = "cryptography-48.0.1-cp314-cp314t-manylinux_2_28_ppc64le.whl", hash = "sha256:e361afba8918070d376df76f408a4f67fec0ee9cff81a99e48fe9a233ef59e17"}, + {file = "cryptography-48.0.1-cp314-cp314t-manylinux_2_28_x86_64.whl", hash = "sha256:d069066deead00ac7f090be101be875a06855908f7ec004c27b8fefb4acfb411"}, + {file = "cryptography-48.0.1-cp314-cp314t-manylinux_2_31_armv7l.whl", hash = "sha256:09f73a725d582cef64b91281a322cd798d14a33b2b6f2b7ad9531dc336d84c02"}, + {file = "cryptography-48.0.1-cp314-cp314t-manylinux_2_34_aarch64.whl", hash = "sha256:15254441469dd6bf027039453288e2072124f8b6603563f5d759e1c9b69273fa"}, + {file = "cryptography-48.0.1-cp314-cp314t-manylinux_2_34_ppc64le.whl", hash = "sha256:8ace4507d1e6533c125f4fac754f8bb8b6a74c08e92179dabd7e16571a3efbf3"}, + {file = "cryptography-48.0.1-cp314-cp314t-manylinux_2_34_x86_64.whl", hash = "sha256:b4e391975f038e66432328639620a4aff2d307513b004f1ca06d6225bced815c"}, + {file = "cryptography-48.0.1-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:42fcd8e26fe555d9b3577a135f5091fefa0aa4e99129c23fb56787a1bd4ada72"}, + {file = "cryptography-48.0.1-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:c1400da5e32a43253392277eac7490a60e497d810a63dd5608d71bbd7af507c9"}, + {file = "cryptography-48.0.1-cp314-cp314t-win32.whl", hash = "sha256:0df56b056bc17c1b7d6821dfa65216e62bd232d8ab05eb3db44e71d235651471"}, + {file = "cryptography-48.0.1-cp314-cp314t-win_amd64.whl", hash = "sha256:9de21387aa95e2a895823d0745b430bed4f33503ba9ab5e0b5311f33e37d66d2"}, + {file = "cryptography-48.0.1-cp39-abi3-macosx_10_9_universal2.whl", hash = "sha256:4fdc69f8e4316bcf0c8c8ec1f26f285d12e8142d88d96c876a59a03be3f6ae67"}, + {file = "cryptography-48.0.1-cp39-abi3-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:48fe40804d4caa2288f24e70ca8c64c42dd826da0ad7e4f1b41b2128d679e6c8"}, + {file = "cryptography-48.0.1-cp39-abi3-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:86be3b1b0b6bf09482fb50a979c508d2950ed95f5621ec77f4e385962006b83a"}, + {file = "cryptography-48.0.1-cp39-abi3-manylinux_2_28_aarch64.whl", hash = "sha256:4ab0a343c807bbcd90c971cd1ecf072937cd01847a9e002bef88fb47ac6be577"}, + {file = "cryptography-48.0.1-cp39-abi3-manylinux_2_28_ppc64le.whl", hash = "sha256:9621de99d2da096006b629979efd8ae7eb2d8b822488d0c89ee4000c306c59b1"}, + {file = "cryptography-48.0.1-cp39-abi3-manylinux_2_28_x86_64.whl", hash = "sha256:88c852a0ae366e262e5a1744b685e6a433dc8788dd2a277e418bf4904203609d"}, + {file = "cryptography-48.0.1-cp39-abi3-manylinux_2_31_armv7l.whl", hash = "sha256:43c5835e2cb98c8733d86f57d6fc879b613f5c3478607281c3e36daffc6dd8a6"}, + {file = "cryptography-48.0.1-cp39-abi3-manylinux_2_34_aarch64.whl", hash = "sha256:fe0180af5bf9236518a087e35bf2d9a347d5f5f51e63c579d683ddff424e3d46"}, + {file = "cryptography-48.0.1-cp39-abi3-manylinux_2_34_ppc64le.whl", hash = "sha256:b7a2d1a937a738a881737cec135a38bb61470589b17515b9f73f571d0ae10401"}, + {file = "cryptography-48.0.1-cp39-abi3-manylinux_2_34_x86_64.whl", hash = "sha256:b74ca3b8e5ecdd833bf6a002ca41b4793bb27fb8f1c06ffaf2643c9e9140e31b"}, + {file = "cryptography-48.0.1-cp39-abi3-musllinux_1_2_aarch64.whl", hash = "sha256:2c37f2461406063b417837f5f3daab668652acd82423efcd7f0a9f04be972de1"}, + {file = "cryptography-48.0.1-cp39-abi3-musllinux_1_2_x86_64.whl", hash = "sha256:86fe77abb1bd87afb251d4d02ada7ecf53a32cee9b67d976abb2e45a13297475"}, + {file = "cryptography-48.0.1-cp39-abi3-win32.whl", hash = "sha256:6b2c0c3e6ccf3ade7750f836ef3ee36eea250cc467d45c256895573ac08cc6f1"}, + {file = "cryptography-48.0.1-cp39-abi3-win_amd64.whl", hash = "sha256:9a49ca6c81417f6a5edb50375a60cccdd70fa0a91a5211829dbea74eba94d2ac"}, + {file = "cryptography-48.0.1-pp311-pypy311_pp73-macosx_11_0_arm64.whl", hash = "sha256:08a597acce1ff37f347400087776599e2348a3a8bc53b44120e463cd274efe4a"}, + {file = "cryptography-48.0.1-pp311-pypy311_pp73-manylinux_2_28_aarch64.whl", hash = "sha256:735824ec41b7f74a7c45fb1591349333e4c696cb6c044e5f46356e560143e4cd"}, + {file = "cryptography-48.0.1-pp311-pypy311_pp73-manylinux_2_28_x86_64.whl", hash = "sha256:92a46e1d638daa264ba2971c0b0489c9409787943efae4d60ffda3d091ef832c"}, + {file = "cryptography-48.0.1-pp311-pypy311_pp73-manylinux_2_34_aarch64.whl", hash = "sha256:7e234ac052af99f2700826a5c29ea99d9c1b1f80341cde62d11c8154dc8e0bd9"}, + {file = "cryptography-48.0.1-pp311-pypy311_pp73-manylinux_2_34_x86_64.whl", hash = "sha256:33842cf0888951cef5bc7ac724ab844a42044c1727b967b7f8997289a0464f92"}, + {file = "cryptography-48.0.1-pp311-pypy311_pp73-win_amd64.whl", hash = "sha256:6184ca7b174f28d7c703f1290d4b297217c45355f77a98f67e9b7f14549ac54a"}, + {file = "cryptography-48.0.1.tar.gz", hash = "sha256:266f4ee051abb2f725b74ef8072b521ce1feacf685a3364fa6a6b45548db791a"}, ] [package.dependencies] From c08776b30c67a8aec502a89b1b14b09847a3308d Mon Sep 17 00:00:00 2001 From: mathieuHa Date: Thu, 18 Jun 2026 15:26:26 +0200 Subject: [PATCH 197/556] fix(logging): emit audit events to the JSON console (#2077) --- warpgate-core/src/logging/json_console.rs | 16 ++++++++++++---- 1 file changed, 12 insertions(+), 4 deletions(-) diff --git a/warpgate-core/src/logging/json_console.rs b/warpgate-core/src/logging/json_console.rs index 51727c9bb..1254b69a4 100644 --- a/warpgate-core/src/logging/json_console.rs +++ b/warpgate-core/src/logging/json_console.rs @@ -28,8 +28,12 @@ where S: Subscriber + for<'a> LookupSpan<'a>, { fn on_event(&self, event: &Event<'_>, ctx: Context<'_, S>) { - // Only log warpgate events (same filter as ValuesLogLayer) - if !event.metadata().target().starts_with("warpgate") { + // Log warpgate events plus audit events (`audit` target). The env + // filter (e.g. `audit=info,warpgate=info`) already gates what reaches + // this layer and the text console layer prints audit events on that + // basis; mirror it here so the JSON console is not missing them. + let target = event.metadata().target(); + if !target.starts_with("warpgate") && target != "audit" { return; } @@ -51,8 +55,12 @@ where // Record event fields event.record(&mut RecordVisitor::new(&mut values)); - // Hide _type from console output (rich JSON field marker, not user-facing) - values.remove("_type"); + // `_type` is a rich-JSON marker normally hidden from console output, + // but for audit events it is the event-type discriminator (e.g. + // "TargetSessionStarted1"), so keep it there. + if target != "audit" { + values.remove("_type"); + } // Extract message before moving values let message = values.remove("message").unwrap_or_default(); From 6a4787991cf4a1bdef2e52438a98dfce204c3714 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Thu, 18 Jun 2026 15:26:50 +0200 Subject: [PATCH 198/556] Bump the version-bumps group across 1 directory with 4 updates (#2068) Signed-off-by: dependabot[bot] --- warpgate-web/package-lock.json | 177 +++++++++++++++++---------------- warpgate-web/package.json | 8 +- 2 files changed, 98 insertions(+), 87 deletions(-) diff --git a/warpgate-web/package-lock.json b/warpgate-web/package-lock.json index 6804c0e40..d9116b88f 100644 --- a/warpgate-web/package-lock.json +++ b/warpgate-web/package-lock.json @@ -18,7 +18,7 @@ "@fortawesome/free-brands-svg-icons": "^7.2.0", "@fortawesome/free-regular-svg-icons": "^7.2.0", "@fortawesome/free-solid-svg-icons": "^7.2.0", - "@openapitools/openapi-generator-cli": "^2.34.0", + "@openapitools/openapi-generator-cli": "^2.35.0", "@otplib/plugin-base32-enc-dec": "^12.0.1", "@otplib/plugin-crypto-js": "^12.0.1", "@otplib/preset-browser": "^12.0.1", @@ -46,8 +46,8 @@ "qrcode": "^1.5.4", "rxjs": "^7.8.2", "sass": "1.78", - "svelte": "^5.56.1", - "svelte-check": "^4.5.0", + "svelte": "^5.56.3", + "svelte-check": "^4.6.0", "svelte-fa": "^4.0.4", "svelte-intersection-observer": "^1.1.1", "svelte-observable": "^0.4.0", @@ -56,7 +56,7 @@ "thenby": "^1.4.1", "tslib": "^2.8.0", "typescript": "^5.9.3", - "typescript-eslint": "^8.60.1", + "typescript-eslint": "^8.61.0", "ua-parser-js": "^2.0.10", "vite": "^7.3.1", "vite-tsconfig-paths": "^6.1.1", @@ -1055,9 +1055,9 @@ } }, "node_modules/@nestjs/common": { - "version": "11.1.21", - "resolved": "https://registry.npmjs.org/@nestjs/common/-/common-11.1.21.tgz", - "integrity": "sha512-YV1HYDGsm2rnR0vrLKidtrG6jYX5yqiIjeur1j8++dKGqhhsJ6cjMs0RfQRSTUH7IjgDemA59/znQ8nRrE0D9g==", + "version": "11.1.24", + "resolved": "https://registry.npmjs.org/@nestjs/common/-/common-11.1.24.tgz", + "integrity": "sha512-9zHxaDDM+oXW9As6UsP5yYB+UqczBmpeSCIFWdPEtEukMnZhxODG1BBjaUcdBB8Sc1uzojSJSJlp3yFp853t1g==", "dev": true, "license": "MIT", "dependencies": { @@ -1087,9 +1087,9 @@ } }, "node_modules/@nestjs/core": { - "version": "11.1.21", - "resolved": "https://registry.npmjs.org/@nestjs/core/-/core-11.1.21.tgz", - "integrity": "sha512-fqo0BHgny3MOuAL8GSfG3ZUKFVVBaBQD/0iyibnwTONT5vPexjQxJzu+945iloVvBDmrnAaRWxC1gqCDEs/AXQ==", + "version": "11.1.24", + "resolved": "https://registry.npmjs.org/@nestjs/core/-/core-11.1.24.tgz", + "integrity": "sha512-K4bzT+lEdd0Hhcsw3jtk56QAW6s6skK3ViN7hIROSN0kUf4ROwWEAKopJID6yhPQxB45kDtP2wEcjzE8171J3g==", "dev": true, "hasInstallScript": true, "license": "MIT", @@ -1185,17 +1185,17 @@ "license": "MIT" }, "node_modules/@openapitools/openapi-generator-cli": { - "version": "2.34.0", - "resolved": "https://registry.npmjs.org/@openapitools/openapi-generator-cli/-/openapi-generator-cli-2.34.0.tgz", - "integrity": "sha512-Z6400REeiq16xkRrdKgtv8nY3xy0DhUnc42DIT5rWjzoj7l4qn+MwqsOqUVJ5UwOF9VUBQGKN7jrDaEkKfj3kQ==", + "version": "2.35.0", + "resolved": "https://registry.npmjs.org/@openapitools/openapi-generator-cli/-/openapi-generator-cli-2.35.0.tgz", + "integrity": "sha512-/nPu0CMeyWUGchEPcLxI+XQ4b1J0sr2fAXJgemVGWv9IA/OlvJu71ZZh3i6vO/201a6nZdD6Xg3TNS0U2G0hmw==", "dev": true, "hasInstallScript": true, "license": "Apache-2.0", "dependencies": { "@inquirer/select": "1.3.3", "@nestjs/axios": "4.0.1", - "@nestjs/common": "11.1.21", - "@nestjs/core": "11.1.21", + "@nestjs/common": "11.1.24", + "@nestjs/core": "11.1.24", "@nuxtjs/opencollective": "0.3.2", "axios": "^1.16.1", "chalk": "4.1.2", @@ -1666,6 +1666,16 @@ "acorn": "^8.9.0" } }, + "node_modules/@sveltejs/load-config": { + "version": "0.1.1", + "resolved": "https://registry.npmjs.org/@sveltejs/load-config/-/load-config-0.1.1.tgz", + "integrity": "sha512-BXXm+VOH/9X4N7Dd1iZ2MqA1h7M+9i2noI8QYuLDY8QcN2WHYn7D/VK/+IJNfcAmRw7ACNJ538UT9GXIhnBTiA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 18.0.0" + } + }, "node_modules/@sveltejs/vite-plugin-svelte": { "version": "6.2.4", "resolved": "https://registry.npmjs.org/@sveltejs/vite-plugin-svelte/-/vite-plugin-svelte-6.2.4.tgz", @@ -1886,17 +1896,17 @@ "license": "MIT" }, "node_modules/@typescript-eslint/eslint-plugin": { - "version": "8.60.1", - "resolved": "https://registry.npmjs.org/@typescript-eslint/eslint-plugin/-/eslint-plugin-8.60.1.tgz", - "integrity": "sha512-JQ4S5GB0tfjO8BuJ4fcX+HodkzJjYBV+7OJ+wLygaX7OGQ7FudyHL4NSCA6ob+w3Yn+5MkKIozOwQhXeM7opVg==", + "version": "8.61.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/eslint-plugin/-/eslint-plugin-8.61.0.tgz", + "integrity": "sha512-bFNvl9ZczlVb+wR2Akszf3gHfKVj/8WanXaGJ3UstTA7brNKg0cNdk6X1Psu5V7MZ2oQtzZKOEzIUehaoxbDGw==", "dev": true, "license": "MIT", "dependencies": { "@eslint-community/regexpp": "^4.12.2", - "@typescript-eslint/scope-manager": "8.60.1", - "@typescript-eslint/type-utils": "8.60.1", - "@typescript-eslint/utils": "8.60.1", - "@typescript-eslint/visitor-keys": "8.60.1", + "@typescript-eslint/scope-manager": "8.61.0", + "@typescript-eslint/type-utils": "8.61.0", + "@typescript-eslint/utils": "8.61.0", + "@typescript-eslint/visitor-keys": "8.61.0", "ignore": "^7.0.5", "natural-compare": "^1.4.0", "ts-api-utils": "^2.5.0" @@ -1909,7 +1919,7 @@ "url": "https://opencollective.com/typescript-eslint" }, "peerDependencies": { - "@typescript-eslint/parser": "^8.60.1", + "@typescript-eslint/parser": "^8.61.0", "eslint": "^8.57.0 || ^9.0.0 || ^10.0.0", "typescript": ">=4.8.4 <6.1.0" } @@ -1925,16 +1935,16 @@ } }, "node_modules/@typescript-eslint/parser": { - "version": "8.60.1", - "resolved": "https://registry.npmjs.org/@typescript-eslint/parser/-/parser-8.60.1.tgz", - "integrity": "sha512-A0M6ua6H252bVjPvvtSgl2QA4+ET9S5Mtkb2GDyTxIhH/C4qDItT7RQNO5PhMC6NXGYXOR9dIalcDDgBKT7oFA==", + "version": "8.61.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/parser/-/parser-8.61.0.tgz", + "integrity": "sha512-5B7PfA2e1NQGCnDHd/0lW7W3gvp3d59Ryw54FYO8Uswxo9f6ikw3AZV+Xj/TvpImmpsiYyUqAfhC6kJID1jF6w==", "dev": true, "license": "MIT", "dependencies": { - "@typescript-eslint/scope-manager": "8.60.1", - "@typescript-eslint/types": "8.60.1", - "@typescript-eslint/typescript-estree": "8.60.1", - "@typescript-eslint/visitor-keys": "8.60.1", + "@typescript-eslint/scope-manager": "8.61.0", + "@typescript-eslint/types": "8.61.0", + "@typescript-eslint/typescript-estree": "8.61.0", + "@typescript-eslint/visitor-keys": "8.61.0", "debug": "^4.4.3" }, "engines": { @@ -1968,14 +1978,14 @@ } }, "node_modules/@typescript-eslint/project-service": { - "version": "8.60.1", - "resolved": "https://registry.npmjs.org/@typescript-eslint/project-service/-/project-service-8.60.1.tgz", - "integrity": "sha512-eXkTH2bxmXlqD1RnOPmLZ9ZM9D3VwSx04JOwBnP9RQ+yUA5a2Mu7SfW8uaV2Aon53NJzZlZYuX7tn91Izf+xaw==", + "version": "8.61.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/project-service/-/project-service-8.61.0.tgz", + "integrity": "sha512-DV42F7MLJO6Rax7SK1yg43tcnEfGUrurSpSxKuVX+a3RCTzBlH3fuxprrOJXKCJGAaw82xXocikJ0uQaqwXgGA==", "dev": true, "license": "MIT", "dependencies": { - "@typescript-eslint/tsconfig-utils": "^8.60.1", - "@typescript-eslint/types": "^8.60.1", + "@typescript-eslint/tsconfig-utils": "^8.61.0", + "@typescript-eslint/types": "^8.61.0", "debug": "^4.4.3" }, "engines": { @@ -2008,14 +2018,14 @@ } }, "node_modules/@typescript-eslint/scope-manager": { - "version": "8.60.1", - "resolved": "https://registry.npmjs.org/@typescript-eslint/scope-manager/-/scope-manager-8.60.1.tgz", - "integrity": "sha512-gvI5OQoptnxQnchOirukCuQ55svJSTuD/4k5+pC267xyBtYry748R9/c3tYUzb/iE6RZfllRz2lVulLCHkTm4w==", + "version": "8.61.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/scope-manager/-/scope-manager-8.61.0.tgz", + "integrity": "sha512-IWdXFHFSb6mlC3HPc7QsLDm5zYEbUla6trDEHf32D3/dnuUyXd87plScSNXSbm0/RxMvObpI17sv/EDTGrGZkA==", "dev": true, "license": "MIT", "dependencies": { - "@typescript-eslint/types": "8.60.1", - "@typescript-eslint/visitor-keys": "8.60.1" + "@typescript-eslint/types": "8.61.0", + "@typescript-eslint/visitor-keys": "8.61.0" }, "engines": { "node": "^18.18.0 || ^20.9.0 || >=21.1.0" @@ -2026,9 +2036,9 @@ } }, "node_modules/@typescript-eslint/tsconfig-utils": { - "version": "8.60.1", - "resolved": "https://registry.npmjs.org/@typescript-eslint/tsconfig-utils/-/tsconfig-utils-8.60.1.tgz", - "integrity": "sha512-nh8w4qAteiKuZu3pSSzG/yGKpw0OlkrKnzFmbVRenKaD4qc+7i1GrmZaLVkr8rk4uipiPGMOW4YsM6WmKZ5CvA==", + "version": "8.61.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/tsconfig-utils/-/tsconfig-utils-8.61.0.tgz", + "integrity": "sha512-O5Amvdv9ztMpxpf+vmFULGG78IE6Qwdr3bCGvqwG4nwc9H2qXkOYJJnRbRHyMkQTjv1d03olqwwwzHLMqpFePQ==", "dev": true, "license": "MIT", "engines": { @@ -2043,15 +2053,15 @@ } }, "node_modules/@typescript-eslint/type-utils": { - "version": "8.60.1", - "resolved": "https://registry.npmjs.org/@typescript-eslint/type-utils/-/type-utils-8.60.1.tgz", - "integrity": "sha512-sdwTrpjosW7ANQYJ39ZBF1ZyEMEGVB2UsikrserVM/30a/F1dTLnu9bGxEdosugyu5caigjLrR2qiD11asjI1A==", + "version": "8.61.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/type-utils/-/type-utils-8.61.0.tgz", + "integrity": "sha512-TuBiQYIkd97yBfInHCTKVYMbX4kvEmpOEuixIuzCU9p8BGT1SfyyO0d0IfDMbPIHcjn/hWnusUX5e8v5Xg+X8A==", "dev": true, "license": "MIT", "dependencies": { - "@typescript-eslint/types": "8.60.1", - "@typescript-eslint/typescript-estree": "8.60.1", - "@typescript-eslint/utils": "8.60.1", + "@typescript-eslint/types": "8.61.0", + "@typescript-eslint/typescript-estree": "8.61.0", + "@typescript-eslint/utils": "8.61.0", "debug": "^4.4.3", "ts-api-utils": "^2.5.0" }, @@ -2086,9 +2096,9 @@ } }, "node_modules/@typescript-eslint/types": { - "version": "8.60.1", - "resolved": "https://registry.npmjs.org/@typescript-eslint/types/-/types-8.60.1.tgz", - "integrity": "sha512-4h0tY8ppCkdCzcrl2YM5M3my0xsE1Tf8om3owEu5oPWmXwkKRmk0j0LGDzYBGUcAlesEbxBhazqu/K4cu3Ug7w==", + "version": "8.61.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/types/-/types-8.61.0.tgz", + "integrity": "sha512-9QTQpZ5Iin4CdIodfbDQFSeiSJKidgYJYug1P9CC2xWgUTvlmixViqDZNciMjwLBZyJnG4tGmPl97rVAFb1AJg==", "devOptional": true, "license": "MIT", "engines": { @@ -2100,16 +2110,16 @@ } }, "node_modules/@typescript-eslint/typescript-estree": { - "version": "8.60.1", - "resolved": "https://registry.npmjs.org/@typescript-eslint/typescript-estree/-/typescript-estree-8.60.1.tgz", - "integrity": "sha512-alpRkfG8hlVE5kdJW2GkfgDgXxold3e8e4l6EnmhRmRLbekgAPCCGDVD++sABy9FcgPFroq+uFcCSM1vR57Cew==", + "version": "8.61.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/typescript-estree/-/typescript-estree-8.61.0.tgz", + "integrity": "sha512-42zatd5qSvvcV1JdDBCLxYRznvP4eIHpPoZXdkPFnAmanA4FuZ5dibSnCBggY8hQnqajPpoGjXFdZ7fIJKQnlA==", "dev": true, "license": "MIT", "dependencies": { - "@typescript-eslint/project-service": "8.60.1", - "@typescript-eslint/tsconfig-utils": "8.60.1", - "@typescript-eslint/types": "8.60.1", - "@typescript-eslint/visitor-keys": "8.60.1", + "@typescript-eslint/project-service": "8.61.0", + "@typescript-eslint/tsconfig-utils": "8.61.0", + "@typescript-eslint/types": "8.61.0", + "@typescript-eslint/visitor-keys": "8.61.0", "debug": "^4.4.3", "minimatch": "^10.2.2", "semver": "^7.7.3", @@ -2146,16 +2156,16 @@ } }, "node_modules/@typescript-eslint/utils": { - "version": "8.60.1", - "resolved": "https://registry.npmjs.org/@typescript-eslint/utils/-/utils-8.60.1.tgz", - "integrity": "sha512-h2MPBLoNtjc3qZWfY3Tl51yPorQ2McHn8pJfcMNTcIvrrZrr90Ykffit0yjrPFWQcRcUxzH20+6OcVdW4yHtUg==", + "version": "8.61.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/utils/-/utils-8.61.0.tgz", + "integrity": "sha512-3bzFt7ImFMW/jVYwJamDoe/dMOdFLSC6pom6rRjdh4SZJEYupyMzem8e7vKZLclLfpHjlwSAXOUxtKxGXUiLqA==", "dev": true, "license": "MIT", "dependencies": { "@eslint-community/eslint-utils": "^4.9.1", - "@typescript-eslint/scope-manager": "8.60.1", - "@typescript-eslint/types": "8.60.1", - "@typescript-eslint/typescript-estree": "8.60.1" + "@typescript-eslint/scope-manager": "8.61.0", + "@typescript-eslint/types": "8.61.0", + "@typescript-eslint/typescript-estree": "8.61.0" }, "engines": { "node": "^18.18.0 || ^20.9.0 || >=21.1.0" @@ -2170,13 +2180,13 @@ } }, "node_modules/@typescript-eslint/visitor-keys": { - "version": "8.60.1", - "resolved": "https://registry.npmjs.org/@typescript-eslint/visitor-keys/-/visitor-keys-8.60.1.tgz", - "integrity": "sha512-EbGRQg4FhrmwLodl+t3JNAnXHWVr9Vp+Zl1QBZVPY4ByfkzIT8cX3K6QWODHtkIZqqJVEWvhHSx3v5PDHsaQag==", + "version": "8.61.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/visitor-keys/-/visitor-keys-8.61.0.tgz", + "integrity": "sha512-QVLZu3ZPQEE+HICQyAMZ2yLQhxf0meY/wx6Hx14YcTNj13JB3qHlX3lJ02L3fLGHgERRH71kvYDwiXIguT3AjQ==", "dev": true, "license": "MIT", "dependencies": { - "@typescript-eslint/types": "8.60.1", + "@typescript-eslint/types": "8.61.0", "eslint-visitor-keys": "^5.0.0" }, "engines": { @@ -7382,9 +7392,9 @@ } }, "node_modules/svelte": { - "version": "5.56.1", - "resolved": "https://registry.npmjs.org/svelte/-/svelte-5.56.1.tgz", - "integrity": "sha512-eArsJmvl3xZVuTYD852PzIEdg2wgDdIZ1NEsIPbzAukHwi284B18No4nK2rCO9AwsWUDza4Cjvmoa4HaojTl5g==", + "version": "5.56.3", + "resolved": "https://registry.npmjs.org/svelte/-/svelte-5.56.3.tgz", + "integrity": "sha512-w7JvrM5IFl5cmfbY0TLik9o7mjRUJmRMhOR51tBPu708Gr/MjbGs7VnJnr/B0CaXeI4vtnOh7RKxDr0cwhMdDA==", "license": "MIT", "dependencies": { "@jridgewell/remapping": "^2.3.4", @@ -7398,7 +7408,7 @@ "clsx": "^2.1.1", "devalue": "^5.8.1", "esm-env": "^1.2.1", - "esrap": "^2.2.9", + "esrap": "^2.2.11", "is-reference": "^3.0.3", "locate-character": "^3.0.0", "magic-string": "^0.30.11", @@ -7409,13 +7419,14 @@ } }, "node_modules/svelte-check": { - "version": "4.5.0", - "resolved": "https://registry.npmjs.org/svelte-check/-/svelte-check-4.5.0.tgz", - "integrity": "sha512-9lNwPxCLWniFvQIcEv1LFqjIxcFtO3smb5+5BKbRJ3ttL4o2lXCej5rLF4DAnfLPI66oaA81vAxw6ILdIWI7kA==", + "version": "4.6.0", + "resolved": "https://registry.npmjs.org/svelte-check/-/svelte-check-4.6.0.tgz", + "integrity": "sha512-KhVnDFDSid57mmZtHz8gfW8AAGylOZ0vPnOIzVmAL+urzwK8sBYXRss953gD8T0OdgAQ11mdWhE6uadmtOz8TQ==", "dev": true, "license": "MIT", "dependencies": { "@jridgewell/trace-mapping": "^0.3.25", + "@sveltejs/load-config": "0.1.1", "chokidar": "^4.0.1", "fdir": "^6.2.0", "picocolors": "^1.0.0", @@ -7842,16 +7853,16 @@ } }, "node_modules/typescript-eslint": { - "version": "8.60.1", - "resolved": "https://registry.npmjs.org/typescript-eslint/-/typescript-eslint-8.60.1.tgz", - "integrity": "sha512-6m5hkkRAp8lKvhVpcprAIn5KkehQEh+47oHH2VGnExEh7dhNxXlg6GPAOIu6TxbVQxhebrJDvjl3020ooiWCMA==", + "version": "8.61.0", + "resolved": "https://registry.npmjs.org/typescript-eslint/-/typescript-eslint-8.61.0.tgz", + "integrity": "sha512-8y31Rd0eGTrDKqhy6vT0HtzhN+YLjQizwX3aA3hPXP/ynSfnrBXcQY5IzsP9/DM7+klX4IUncZZjkchP0z+rUw==", "dev": true, "license": "MIT", "dependencies": { - "@typescript-eslint/eslint-plugin": "8.60.1", - "@typescript-eslint/parser": "8.60.1", - "@typescript-eslint/typescript-estree": "8.60.1", - "@typescript-eslint/utils": "8.60.1" + "@typescript-eslint/eslint-plugin": "8.61.0", + "@typescript-eslint/parser": "8.61.0", + "@typescript-eslint/typescript-estree": "8.61.0", + "@typescript-eslint/utils": "8.61.0" }, "engines": { "node": "^18.18.0 || ^20.9.0 || >=21.1.0" diff --git a/warpgate-web/package.json b/warpgate-web/package.json index 8a1170ba5..14a30f431 100644 --- a/warpgate-web/package.json +++ b/warpgate-web/package.json @@ -25,7 +25,7 @@ "@fortawesome/free-brands-svg-icons": "^7.2.0", "@fortawesome/free-regular-svg-icons": "^7.2.0", "@fortawesome/free-solid-svg-icons": "^7.2.0", - "@openapitools/openapi-generator-cli": "^2.34.0", + "@openapitools/openapi-generator-cli": "^2.35.0", "@otplib/plugin-base32-enc-dec": "^12.0.1", "@otplib/plugin-crypto-js": "^12.0.1", "@otplib/preset-browser": "^12.0.1", @@ -53,8 +53,8 @@ "qrcode": "^1.5.4", "rxjs": "^7.8.2", "sass": "1.78", - "svelte": "^5.56.1", - "svelte-check": "^4.5.0", + "svelte": "^5.56.3", + "svelte-check": "^4.6.0", "svelte-fa": "^4.0.4", "svelte-intersection-observer": "^1.1.1", "svelte-observable": "^0.4.0", @@ -63,7 +63,7 @@ "thenby": "^1.4.1", "tslib": "^2.8.0", "typescript": "^5.9.3", - "typescript-eslint": "^8.60.1", + "typescript-eslint": "^8.61.0", "ua-parser-js": "^2.0.10", "vite": "^7.3.1", "vite-tsconfig-paths": "^6.1.1", From a1e6250f5d7eef324f05e739ac9d12fff6ad720a Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Thu, 18 Jun 2026 15:27:22 +0200 Subject: [PATCH 199/556] Bump the version-bumps group across 1 directory with 7 updates (#2078) Signed-off-by: dependabot[bot] --- Cargo.lock | 129 ++++++++++++++++++++++++++--------------------------- 1 file changed, 64 insertions(+), 65 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index 72556b089..158dcaf4e 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -375,7 +375,7 @@ dependencies = [ "aws-types", "bytes", "fastrand", - "http 1.4.1", + "http 1.4.2", "time", "tokio", "tracing", @@ -434,7 +434,7 @@ dependencies = [ "bytes", "bytes-utils", "fastrand", - "http 1.4.1", + "http 1.4.2", "http-body 1.0.1", "percent-encoding", "pin-project-lite", @@ -444,9 +444,9 @@ dependencies = [ [[package]] name = "aws-sdk-ec2" -version = "1.229.0" +version = "1.231.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8bb5dfffc70e73ed387efa49d11fa455ef41dfb19df2ca41abafbbf8bb60c9d1" +checksum = "7ab7139530b6a081a73eeeb72f4c2b1371b2d87a42c4432528bff13c88896766" dependencies = [ "arc-swap", "aws-credential-types", @@ -463,7 +463,7 @@ dependencies = [ "aws-types", "fastrand", "http 0.2.12", - "http 1.4.1", + "http 1.4.2", "regex-lite", "tracing", ] @@ -488,7 +488,7 @@ dependencies = [ "bytes", "fastrand", "http 0.2.12", - "http 1.4.1", + "http 1.4.2", "regex-lite", "tracing", ] @@ -513,7 +513,7 @@ dependencies = [ "bytes", "fastrand", "http 0.2.12", - "http 1.4.1", + "http 1.4.2", "regex-lite", "tracing", ] @@ -540,7 +540,7 @@ dependencies = [ "aws-types", "fastrand", "http 0.2.12", - "http 1.4.1", + "http 1.4.2", "regex-lite", "tracing", "url", @@ -567,7 +567,7 @@ dependencies = [ "aws-types", "fastrand", "http 0.2.12", - "http 1.4.1", + "http 1.4.2", "regex-lite", "tracing", ] @@ -587,7 +587,7 @@ dependencies = [ "hex", "hmac 0.13.0", "http 0.2.12", - "http 1.4.1", + "http 1.4.2", "percent-encoding", "sha2 0.11.0", "time", @@ -617,7 +617,7 @@ dependencies = [ "bytes-utils", "futures-core", "futures-util", - "http 1.4.1", + "http 1.4.2", "http-body 1.0.1", "http-body-util", "percent-encoding", @@ -638,7 +638,7 @@ dependencies = [ "h2 0.3.27", "h2 0.4.14", "http 0.2.12", - "http 1.4.1", + "http 1.4.2", "http-body 0.4.6", "hyper 0.14.32", "hyper 1.10.1", @@ -702,7 +702,7 @@ dependencies = [ "bytes", "fastrand", "http 0.2.12", - "http 1.4.1", + "http 1.4.2", "http-body 0.4.6", "http-body 1.0.1", "http-body-util", @@ -723,7 +723,7 @@ dependencies = [ "aws-smithy-types", "bytes", "http 0.2.12", - "http 1.4.1", + "http 1.4.2", "pin-project-lite", "tokio", "tracing", @@ -749,7 +749,7 @@ checksum = "7442cb268338f0eb8278140a107c046756aa01093d8ef5e99628d34ae09c94f5" dependencies = [ "aws-smithy-runtime-api", "aws-smithy-types", - "http 1.4.1", + "http 1.4.2", ] [[package]] @@ -762,7 +762,7 @@ dependencies = [ "bytes", "bytes-utils", "http 0.2.12", - "http 1.4.1", + "http 1.4.2", "http-body 0.4.6", "http-body 1.0.1", "http-body-util", @@ -808,7 +808,7 @@ dependencies = [ "axum-core", "bytes", "futures-util", - "http 1.4.1", + "http 1.4.2", "http-body 1.0.1", "http-body-util", "itoa", @@ -832,7 +832,7 @@ checksum = "08c78f31d7b1291f7ee735c1c6780ccde7785daae9a9206026862dab7d8792d1" dependencies = [ "bytes", "futures-core", - "http 1.4.1", + "http 1.4.2", "http-body 1.0.1", "http-body-util", "mime", @@ -1858,7 +1858,6 @@ version = "0.5.8" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "7cd812cc2bc1d69d4764bd80df88b4317eaef9e773c75226407d9bc0876b211c" dependencies = [ - "powerfmt", "serde_core", ] @@ -2633,7 +2632,7 @@ dependencies = [ "fnv", "futures-core", "futures-sink", - "http 1.4.1", + "http 1.4.2", "indexmap 2.14.0", "slab", "tokio", @@ -2726,7 +2725,7 @@ dependencies = [ "base64 0.22.1", "bytes", "headers-core", - "http 1.4.1", + "http 1.4.2", "httpdate", "mime", "sha1 0.10.6", @@ -2738,7 +2737,7 @@ version = "0.3.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "54b4a22553d4242c49fddb9ba998a99962b5cc6f22cb5a3482bec22522403ce4" dependencies = [ - "http 1.4.1", + "http 1.4.2", ] [[package]] @@ -2823,9 +2822,9 @@ dependencies = [ [[package]] name = "http" -version = "1.4.1" +version = "1.4.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8be7462df143984c4598a256ef469b251d7d7f9e271135073e78fc535414f3d0" +checksum = "6970f50e31d6fc17d3fa27329444bfa74e196cf62e95052a3f6fee181dba6425" dependencies = [ "bytes", "itoa", @@ -2849,7 +2848,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "1efedce1fb8e6913f23e0c92de8e62cd5b772a67e7b3946df930a62566c93184" dependencies = [ "bytes", - "http 1.4.1", + "http 1.4.2", ] [[package]] @@ -2860,7 +2859,7 @@ checksum = "b021d93e26becf5dc7e1b75b1bed1fd93124b374ceb73f43d4d4eafec896a64a" dependencies = [ "bytes", "futures-core", - "http 1.4.1", + "http 1.4.2", "http-body 1.0.1", "pin-project-lite", ] @@ -2949,7 +2948,7 @@ dependencies = [ "futures-channel", "futures-core", "h2 0.4.14", - "http 1.4.1", + "http 1.4.2", "http-body 1.0.1", "httparse", "httpdate", @@ -2981,7 +2980,7 @@ version = "0.27.9" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "33ca68d021ef39cf6463ab54c1d0f5daf03377b70561305bb89a8f83aab66e0f" dependencies = [ - "http 1.4.1", + "http 1.4.2", "hyper 1.10.1", "hyper-util", "rustls 0.23.40", @@ -3014,7 +3013,7 @@ dependencies = [ "bytes", "futures-channel", "futures-util", - "http 1.4.1", + "http 1.4.2", "http-body 1.0.1", "hyper 1.10.1", "ipnet", @@ -3703,9 +3702,9 @@ checksum = "ae960838283323069879657ca3de837e9f7bbb4c7bf6ea7f1b290d5e9476d2e0" [[package]] name = "memchr" -version = "2.8.1" +version = "2.8.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "6b947ae49db0d222b1dbc6b113ce7248a3fc3a6ca21b696717bfc000ba4484d8" +checksum = "88904434abc2901f197fe8cc55f0445e7ded921dba5911dad2e2b39b48e663c4" [[package]] name = "memmem" @@ -3800,7 +3799,7 @@ dependencies = [ "bytes", "encoding_rs", "futures-util", - "http 1.4.1", + "http 1.4.2", "httparse", "memchr", "mime", @@ -4018,10 +4017,10 @@ version = "5.0.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "51e219e79014df21a225b1860a479e2dcd7cbd9130f4defd4bd0e191ea31d67d" dependencies = [ - "base64 0.21.7", + "base64 0.22.1", "chrono", "getrandom 0.2.17", - "http 1.4.1", + "http 1.4.2", "rand 0.8.6", "reqwest 0.12.28", "serde", @@ -4070,7 +4069,7 @@ dependencies = [ "dyn-clone", "ed25519-dalek 2.2.0", "hmac 0.12.1", - "http 1.4.1", + "http 1.4.2", "itertools 0.10.5", "log", "oauth2", @@ -4669,7 +4668,7 @@ dependencies = [ "futures-util", "headers", "hex", - "http 1.4.1", + "http 1.4.2", "http-body-util", "httpdate", "hyper 1.10.1", @@ -4754,7 +4753,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "41273b691a3d467a8c44d05506afba9f7b6bd56c9cdf80123de13fe52d7ec587" dependencies = [ "darling 0.20.11", - "http 1.4.1", + "http 1.4.2", "indexmap 2.14.0", "mime", "proc-macro-crate", @@ -5152,15 +5151,16 @@ dependencies = [ [[package]] name = "ratatui" -version = "0.30.0" +version = "0.30.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d1ce67fb8ba4446454d1c8dbaeda0557ff5e94d39d5e5ed7f10a65eb4c8266bc" +checksum = "1695748e3a735b34968c887ceea5a380b43545903868ae8f5b666593100f6b68" dependencies = [ "instability", "ratatui-core", "ratatui-crossterm", "ratatui-termion", "ratatui-widgets", + "serde", ] [[package]] @@ -5292,9 +5292,9 @@ dependencies = [ [[package]] name = "regex" -version = "1.12.3" +version = "1.12.4" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e10754a14b9137dd7b1e3e5b0493cc9171fdd105e0ab477f51b72e7f3ac0e276" +checksum = "f1292b7759ae1cb9ec195452d1390a074f0cd8541ab7a5a8c31cd6db45d4a6ba" dependencies = [ "aho-corasick", "memchr", @@ -5321,9 +5321,9 @@ checksum = "cab834c73d247e67f4fae452806d17d3c7501756d98c8808d7c9c7aa7d18f973" [[package]] name = "regex-syntax" -version = "0.8.10" +version = "0.8.11" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "dc897dd8d9e8bd1ed8cdad82b5966c3e0ecae09fb1907d58efaa013543185d0a" +checksum = "d6f6ff9a378485b298a5286656da665ba74413d36db0979633275d2e708145d4" [[package]] name = "reqwest" @@ -5336,7 +5336,7 @@ dependencies = [ "futures-core", "futures-util", "h2 0.4.14", - "http 1.4.1", + "http 1.4.2", "http-body 1.0.1", "http-body-util", "hyper 1.10.1", @@ -5377,7 +5377,7 @@ dependencies = [ "futures-core", "futures-util", "h2 0.4.14", - "http 1.4.1", + "http 1.4.2", "http-body 1.0.1", "http-body-util", "hyper 1.10.1", @@ -6954,7 +6954,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "32497e9a4c7b38532efcdebeef879707aa9f794296a4f0244f6f69e9bc8574bd" dependencies = [ "fastrand", - "getrandom 0.3.4", + "getrandom 0.4.2", "once_cell", "rustix 1.1.4", "windows-sys 0.61.2", @@ -7093,12 +7093,11 @@ dependencies = [ [[package]] name = "time" -version = "0.3.47" +version = "0.3.49" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "743bd48c283afc0388f9b8827b976905fb217ad9e647fae3a379a9283c4def2c" +checksum = "711a53c2d47bbd818258c498c8dbfe186a2526c631495cfe7e078567f86b8469" dependencies = [ "deranged", - "itoa", "libc", "num-conv", "num_threads", @@ -7110,15 +7109,15 @@ dependencies = [ [[package]] name = "time-core" -version = "0.1.8" +version = "0.1.9" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7694e1cfe791f8d31026952abf09c69ca6f6fa4e1a1229e18988f06a04a12dca" +checksum = "9e1c906769ad99c88eaa54e728060edef082f8e358ff32030cb7c7d315e81109" [[package]] name = "time-macros" -version = "0.2.27" +version = "0.2.29" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "2e70e4c5a0e0a8a4823ad65dfe1a6930e4f4d756dcd9dd7939022b5e8c501215" +checksum = "71c652a3727a9cbb9a02f707f530b618ce00d0ccd762009c8c23bd191df3c17d" dependencies = [ "num-conv", "time-core", @@ -7312,7 +7311,7 @@ dependencies = [ "base64 0.22.1", "bytes", "h2 0.4.14", - "http 1.4.1", + "http 1.4.2", "http-body 1.0.1", "http-body-util", "hyper 1.10.1", @@ -7386,7 +7385,7 @@ dependencies = [ "bytes", "futures-core", "futures-util", - "http 1.4.1", + "http 1.4.2", "http-body 1.0.1", "http-body-util", "pin-project-lite", @@ -7495,7 +7494,7 @@ checksum = "eadc29d668c91fcc564941132e17b28a7ceb2f3ebf0b9dae3e03fd7a6748eb0d" dependencies = [ "bytes", "data-encoding", - "http 1.4.1", + "http 1.4.2", "httparse", "log", "rand 0.9.4", @@ -7512,7 +7511,7 @@ checksum = "8628dcc84e5a09eb3d8423d6cb682965dea9133204e8fb3efee74c2a0c259442" dependencies = [ "bytes", "data-encoding", - "http 1.4.1", + "http 1.4.2", "httparse", "log", "rand 0.9.4", @@ -7529,7 +7528,7 @@ checksum = "6c01152af293afb9c7c2a57e4b559c5620b421f6d133261c60dd2d0cdb38e6b8" dependencies = [ "bytes", "data-encoding", - "http 1.4.1", + "http 1.4.2", "httparse", "log", "rand 0.9.4", @@ -7716,9 +7715,9 @@ checksum = "06abde3611657adf66d383f00b093d7faecc7fa57071cce2578660c9f1010821" [[package]] name = "uuid" -version = "1.23.2" +version = "1.23.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d258b83ceec21034727ecee8c382cfa6c3e133699b0742c64571814fb420c9f7" +checksum = "144d6b123cef80b301b8f72a9e2ca4370ddec21950d0a103dd22c437006d2db7" dependencies = [ "atomic", "getrandom 0.4.2", @@ -7874,7 +7873,7 @@ dependencies = [ "aws-smithy-runtime-api", "dashmap", "data-encoding", - "http 1.4.1", + "http 1.4.2", "reqwest 0.13.4", "thiserror 2.0.18", "tokio", @@ -8092,7 +8091,7 @@ dependencies = [ "data-encoding", "delegate", "futures", - "http 1.4.1", + "http 1.4.2", "percent-encoding", "poem", "poem-openapi", @@ -8131,7 +8130,7 @@ dependencies = [ "bytes", "dashmap", "futures", - "http 1.4.1", + "http 1.4.2", "md5", "poem", "poem-openapi", @@ -8227,7 +8226,7 @@ dependencies = [ "dialoguer", "ed25519-dalek 2.2.0", "futures", - "ratatui 0.30.0", + "ratatui 0.30.1", "russh", "sea-orm", "serde", @@ -9210,7 +9209,7 @@ checksum = "ef19a12dfb29fe39f78e1547e1be49717b84aef8762a4001359ed4f94d3accc1" dependencies = [ "async-trait", "base64 0.22.1", - "http 1.4.1", + "http 1.4.2", "http-body-util", "hyper 1.10.1", "hyper-rustls 0.27.9", From e8a6ea207fcfb9b7a5b43e1a9fab144058a1fd03 Mon Sep 17 00:00:00 2001 From: Eugene Date: Thu, 18 Jun 2026 17:32:58 +0200 Subject: [PATCH 200/556] fixed #1421 - MySQL/Postgres TLS upgrade race (#2081) --- Cargo.lock | 1 + warpgate-protocol-mysql/src/client.rs | 4 +- warpgate-protocol-mysql/src/session.rs | 3 +- warpgate-protocol-mysql/src/stream.rs | 7 +- warpgate-protocol-postgres/src/client.rs | 7 +- warpgate-protocol-postgres/src/session.rs | 4 +- warpgate-protocol-postgres/src/stream.rs | 6 +- warpgate-tls/Cargo.toml | 1 + warpgate-tls/src/lib.rs | 5 +- warpgate-tls/src/maybe_tls_stream.rs | 135 ++++++++++++++++++++-- 10 files changed, 149 insertions(+), 24 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index 158dcaf4e..4971aa317 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -8271,6 +8271,7 @@ dependencies = [ name = "warpgate-tls" version = "0.25.4" dependencies = [ + "bytes", "poem", "poem-openapi", "rustls 0.23.40", diff --git a/warpgate-protocol-mysql/src/client.rs b/warpgate-protocol-mysql/src/client.rs index 8b2d3d40a..ad1abfd0c 100644 --- a/warpgate-protocol-mysql/src/client.rs +++ b/warpgate-protocol-mysql/src/client.rs @@ -11,14 +11,14 @@ use warpgate_database_protocols::mysql::protocol::connect::{ Handshake, HandshakeResponse, SslRequest, }; use warpgate_database_protocols::mysql::protocol::response::ErrPacket; -use warpgate_tls::{TlsMode, configure_tls_connector}; +use warpgate_tls::{ClientTlsStream, TlsMode, configure_tls_connector}; use crate::common::compute_auth_challenge_response; use crate::error::MySqlError; use crate::stream::MySqlStream; pub struct MySqlClient { - pub stream: MySqlStream>, + pub stream: MySqlStream>, pub _capabilities: Capabilities, } diff --git a/warpgate-protocol-mysql/src/session.rs b/warpgate-protocol-mysql/src/session.rs index 749a25018..0f4d7f405 100644 --- a/warpgate-protocol-mysql/src/session.rs +++ b/warpgate-protocol-mysql/src/session.rs @@ -26,13 +26,14 @@ use warpgate_database_protocols::mysql::protocol::connect::{ }; use warpgate_database_protocols::mysql::protocol::response::{ErrPacket, OkPacket, Status}; use warpgate_database_protocols::mysql::protocol::text::Query; +use warpgate_tls::ServerTlsStream; use crate::client::{ConnectionOptions, MySqlClient}; use crate::error::MySqlError; use crate::stream::MySqlStream; pub struct MySqlSession { - stream: MySqlStream>, + stream: MySqlStream>, capabilities: Capabilities, challenge: [u8; 20], username: Option, diff --git a/warpgate-protocol-mysql/src/stream.rs b/warpgate-protocol-mysql/src/stream.rs index a52a40338..7526de3a3 100644 --- a/warpgate-protocol-mysql/src/stream.rs +++ b/warpgate-protocol-mysql/src/stream.rs @@ -84,7 +84,12 @@ where mut self, config: >::UpgradeConfig, ) -> Result { - self.stream = self.stream.upgrade(config).await?; + // Any data already read off the socket past the last decoded packet + // is the beginning of the TLS handshake (e.g. clients are allowed + // to send their ClientHello right behind the SSLRequest packet) and + // has to be replayed into the TLS layer (#1421). + let leftover = std::mem::take(&mut self.inbound_buffer).freeze(); + self.stream = self.stream.upgrade(config, leftover).await?; Ok(self) } diff --git a/warpgate-protocol-postgres/src/client.rs b/warpgate-protocol-postgres/src/client.rs index 7c7940144..4b2978f15 100644 --- a/warpgate-protocol-postgres/src/client.rs +++ b/warpgate-protocol-postgres/src/client.rs @@ -7,16 +7,15 @@ use pgwire::messages::{DecodeContext, PgWireBackendMessage, ProtocolVersion}; use rsasl::config::SASLConfig; use rsasl::prelude::{Mechname, SASLClient}; use tokio::net::TcpStream; -use tokio_rustls::client::TlsStream; use tracing::{debug, info, warn}; use warpgate_common::{TargetPostgresOptions, WarpgateError}; -use warpgate_tls::{TlsMode, configure_tls_connector}; +use warpgate_tls::{ClientTlsStream, TlsMode, configure_tls_connector}; use crate::error::PostgresError; use crate::stream::{PgWireGenericBackendMessage, PostgresEncode, PostgresStream}; pub struct PostgresClient { - pub stream: PostgresStream>, + pub stream: PostgresStream>, decode_context: DecodeContext, } @@ -203,7 +202,7 @@ impl PostgresClient { } async fn run_sasl_auth( - stream: &mut PostgresStream>, + stream: &mut PostgresStream>, mechanisms: Vec, username: &str, password: &str, diff --git a/warpgate-protocol-postgres/src/session.rs b/warpgate-protocol-postgres/src/session.rs index 17f57884a..034221853 100644 --- a/warpgate-protocol-postgres/src/session.rs +++ b/warpgate-protocol-postgres/src/session.rs @@ -13,7 +13,6 @@ use rustls::ServerConfig; use tokio::io::{AsyncRead, AsyncWrite}; use tokio::sync::Mutex; use tokio::time; -use tokio_rustls::server::TlsStream; use tracing::{debug, error, info, info_span, warn}; use uuid::Uuid; use warpgate_common::auth::{ @@ -25,6 +24,7 @@ use warpgate_core::auth::validate_and_add_credential; use warpgate_core::{ ConfigProvider, Services, WarpgateServerHandle, authorize_ticket, consume_ticket, }; +use warpgate_tls::ServerTlsStream; use crate::client::{ConnectionOptions, PostgresClient}; use crate::error::PostgresError; @@ -34,7 +34,7 @@ use crate::stream::{ }; pub struct PostgresSession { - stream: PostgresStream>, + stream: PostgresStream>, tls_config: Arc, username: Option, database: Option, diff --git a/warpgate-protocol-postgres/src/stream.rs b/warpgate-protocol-postgres/src/stream.rs index e4ca618d6..3746b8e71 100644 --- a/warpgate-protocol-postgres/src/stream.rs +++ b/warpgate-protocol-postgres/src/stream.rs @@ -148,7 +148,11 @@ where mut self, config: >::UpgradeConfig, ) -> Result { - self.stream = self.stream.upgrade(config).await?; + // Any data already read off the socket past the last decoded message + // is the beginning of the TLS handshake and has to be replayed into + // the TLS layer (#1421). + let leftover = std::mem::take(&mut self.inbound_buffer).freeze(); + self.stream = self.stream.upgrade(config, leftover).await?; Ok(self) } } diff --git a/warpgate-tls/Cargo.toml b/warpgate-tls/Cargo.toml index 7c1f638b4..38c48e117 100644 --- a/warpgate-tls/Cargo.toml +++ b/warpgate-tls/Cargo.toml @@ -6,6 +6,7 @@ license = "Apache-2.0" publish = false [dependencies] +bytes.workspace = true poem.workspace = true poem-openapi.workspace = true rustls.workspace = true diff --git a/warpgate-tls/src/lib.rs b/warpgate-tls/src/lib.rs index 3d5306606..92930481c 100644 --- a/warpgate-tls/src/lib.rs +++ b/warpgate-tls/src/lib.rs @@ -7,7 +7,10 @@ mod rustls_root_certs; pub use cert::*; pub use error::*; -pub use maybe_tls_stream::{MaybeTlsStream, MaybeTlsStreamError, UpgradableStream}; +pub use maybe_tls_stream::{ + ClientTlsStream, MaybeTlsStream, MaybeTlsStreamError, PrefixedStream, ServerTlsStream, + UpgradableStream, +}; pub use mode::TlsMode; pub use rustls_helpers::{ResolveServerCert, configure_tls_connector}; pub use rustls_root_certs::ROOT_CERT_STORE; diff --git a/warpgate-tls/src/maybe_tls_stream.rs b/warpgate-tls/src/maybe_tls_stream.rs index 0a86ac53b..0d823deef 100644 --- a/warpgate-tls/src/maybe_tls_stream.rs +++ b/warpgate-tls/src/maybe_tls_stream.rs @@ -1,8 +1,9 @@ use std::future::Future; use std::pin::Pin; use std::sync::Arc; -use std::task::Poll; +use std::task::{Context, Poll}; +use bytes::Bytes; use rustls::pki_types::ServerName; use rustls::{ClientConfig, ServerConfig}; use tokio::io::{AsyncRead, AsyncWrite, ReadBuf}; @@ -15,15 +16,75 @@ pub enum MaybeTlsStreamError { Io(#[from] std::io::Error), } +/// A TLS client stream produced by upgrading `S`. +pub type ClientTlsStream = tokio_rustls::client::TlsStream>; +/// A TLS server stream produced by upgrading `S`. +pub type ServerTlsStream = tokio_rustls::server::TlsStream>; + +/// Replays a buffer and then continues with the underlying stream. +/// +/// Protocols with a midstream TLS upgrade can read a chunk off the socket +/// that already contains the start of the TLS handshake (e.g. a MySQL +/// client may send its ClientHello right behind the SSLRequest packet). +/// `MaybeTlsStream::upgrade` feeds those leftover bytes back into the TLS +/// layer through this wrapper (#1421). +pub struct PrefixedStream { + prefix: Bytes, + inner: S, +} + +impl PrefixedStream { + pub const fn new(inner: S, prefix: Bytes) -> Self { + Self { prefix, inner } + } +} + +impl AsyncRead for PrefixedStream { + fn poll_read( + self: Pin<&mut Self>, + cx: &mut Context<'_>, + buf: &mut ReadBuf<'_>, + ) -> Poll> { + let this = self.get_mut(); + if !this.prefix.is_empty() { + let n = this.prefix.len().min(buf.remaining()); + buf.put_slice(&this.prefix.split_to(n)); + return Poll::Ready(Ok(())); + } + Pin::new(&mut this.inner).poll_read(cx, buf) + } +} + +impl AsyncWrite for PrefixedStream { + fn poll_write( + self: Pin<&mut Self>, + cx: &mut Context<'_>, + buf: &[u8], + ) -> Poll> { + Pin::new(&mut self.get_mut().inner).poll_write(cx, buf) + } + + fn poll_flush(self: Pin<&mut Self>, cx: &mut Context<'_>) -> Poll> { + Pin::new(&mut self.get_mut().inner).poll_flush(cx) + } + + fn poll_shutdown(self: Pin<&mut Self>, cx: &mut Context<'_>) -> Poll> { + Pin::new(&mut self.get_mut().inner).poll_shutdown(cx) + } +} + pub trait UpgradableStream where Self: Sized, T: AsyncRead + AsyncWrite + Unpin, { type UpgradeConfig; + /// Upgrade to TLS; `leftover` is data already read off the stream that + /// belongs to the TLS handshake. fn upgrade( self, config: Self::UpgradeConfig, + leftover: Bytes, ) -> impl Future> + Send; } @@ -55,9 +116,10 @@ where pub async fn upgrade( mut self, tls_config: S::UpgradeConfig, + leftover: Bytes, ) -> Result { if let Self::Raw(stream) = std::mem::replace(&mut self, Self::Upgrading) { - let stream = stream.upgrade(tls_config).await?; + let stream = stream.upgrade(tls_config, leftover).await?; Ok(Self::Tls(stream)) } else { Err(MaybeTlsStreamError::AlreadyUpgraded) @@ -72,7 +134,7 @@ where { fn poll_read( self: Pin<&mut Self>, - cx: &mut std::task::Context<'_>, + cx: &mut Context<'_>, buf: &mut ReadBuf<'_>, ) -> Poll> { match self.get_mut() { @@ -90,7 +152,7 @@ where { fn poll_write( self: Pin<&mut Self>, - cx: &mut std::task::Context<'_>, + cx: &mut Context<'_>, buf: &[u8], ) -> std::task::Poll> { match self.get_mut() { @@ -102,7 +164,7 @@ where fn poll_flush( self: Pin<&mut Self>, - cx: &mut std::task::Context<'_>, + cx: &mut Context<'_>, ) -> std::task::Poll> { match self.get_mut() { Self::Tls(tls) => Pin::new(tls).poll_flush(cx), @@ -113,7 +175,7 @@ where fn poll_shutdown( self: Pin<&mut Self>, - cx: &mut std::task::Context<'_>, + cx: &mut Context<'_>, ) -> std::task::Poll> { match self.get_mut() { Self::Tls(tls) => Pin::new(tls).poll_shutdown(cx), @@ -123,7 +185,7 @@ where } } -impl UpgradableStream> for S +impl UpgradableStream>> for S where S: AsyncRead + AsyncWrite + Unpin + Send, { @@ -132,14 +194,17 @@ where async fn upgrade( self, config: Self::UpgradeConfig, - ) -> Result, MaybeTlsStreamError> { + leftover: Bytes, + ) -> Result>, MaybeTlsStreamError> { let (domain, tls_config) = config; let connector = tokio_rustls::TlsConnector::from(tls_config); - Ok(connector.connect(domain, self).await?) + Ok(connector + .connect(domain, PrefixedStream::new(self, leftover)) + .await?) } } -impl UpgradableStream> for S +impl UpgradableStream>> for S where S: AsyncRead + AsyncWrite + Unpin + Send, { @@ -148,8 +213,54 @@ where async fn upgrade( self, tls_config: Self::UpgradeConfig, - ) -> Result, MaybeTlsStreamError> { + leftover: Bytes, + ) -> Result>, MaybeTlsStreamError> { let acceptor = tokio_rustls::TlsAcceptor::from(tls_config); - Ok(acceptor.accept(self).await?) + Ok(acceptor.accept(PrefixedStream::new(self, leftover)).await?) + } +} + +#[cfg(test)] +mod tests { + use tokio::io::{AsyncReadExt, AsyncWriteExt}; + + use super::*; + + #[tokio::test] + async fn prefixed_stream_replays_prefix_before_inner_data() { + let (mut near, far) = tokio::io::duplex(64); + near.write_all(b" world").await.unwrap(); + + let mut stream = PrefixedStream::new(far, Bytes::from_static(b"hello")); + + let mut buf = [0u8; 11]; + stream.read_exact(&mut buf).await.unwrap(); + assert_eq!(&buf, b"hello world"); + } + + #[tokio::test] + async fn prefixed_stream_serves_prefix_across_small_reads() { + let (_near, far) = tokio::io::duplex(64); + let mut stream = PrefixedStream::new(far, Bytes::from_static(b"abcd")); + + let mut buf = [0u8; 3]; + stream.read_exact(&mut buf).await.unwrap(); + assert_eq!(&buf, b"abc"); + let mut buf = [0u8; 1]; + stream.read_exact(&mut buf).await.unwrap(); + assert_eq!(&buf, b"d"); + } + + #[tokio::test] + async fn prefixed_stream_writes_pass_through() { + let (mut near, far) = tokio::io::duplex(64); + let mut stream = PrefixedStream::new(far, Bytes::from_static(b"unused")); + + stream.write_all(b"ping").await.unwrap(); + stream.flush().await.unwrap(); + + let mut buf = [0u8; 4]; + near.read_exact(&mut buf).await.unwrap(); + assert_eq!(&buf, b"ping"); } } From 27ab127687adc799641edb652f9a1d52c5215dd3 Mon Sep 17 00:00:00 2001 From: Eugene Date: Thu, 18 Jun 2026 17:44:29 +0200 Subject: [PATCH 201/556] fixed #1842 - tell the user when the session is closed due to inactivity (#2082) --- warpgate-protocol-ssh/src/client/mod.rs | 4 +++- warpgate-protocol-ssh/src/server/mod.rs | 3 ++- warpgate-protocol-ssh/src/server/session.rs | 17 +++++++++++++++-- 3 files changed, 20 insertions(+), 4 deletions(-) diff --git a/warpgate-protocol-ssh/src/client/mod.rs b/warpgate-protocol-ssh/src/client/mod.rs index 2ec04be2a..e41283ed2 100644 --- a/warpgate-protocol-ssh/src/client/mod.rs +++ b/warpgate-protocol-ssh/src/client/mod.rs @@ -7,6 +7,7 @@ use std::collections::HashMap; use std::io; use std::net::ToSocketAddrs; use std::sync::Arc; +use std::time::Duration; use anyhow::Result; use bytes::Bytes; @@ -539,7 +540,8 @@ impl RemoteClient { let mut config = russh::client::Config { preferred: algos, nodelay: true, - inactivity_timeout: Some(ssh_config.inactivity_timeout), + // Extra time for the "closing due to inactivity" message to be sent + inactivity_timeout: Some(ssh_config.inactivity_timeout + Duration::from_secs(10)), keepalive_interval: ssh_config.keepalive_interval, ..Default::default() }; diff --git a/warpgate-protocol-ssh/src/server/mod.rs b/warpgate-protocol-ssh/src/server/mod.rs index dbd011bbd..2435ad6e2 100644 --- a/warpgate-protocol-ssh/src/server/mod.rs +++ b/warpgate-protocol-ssh/src/server/mod.rs @@ -114,7 +114,8 @@ async fn _handle_connection( russh::server::Config { auth_rejection_time: Duration::from_secs(1), auth_rejection_time_initial: Some(Duration::from_secs(0)), - inactivity_timeout: Some(config.store.ssh.inactivity_timeout), + // Extra time for the "closing due to inactivity" message to be sent + inactivity_timeout: Some(config.store.ssh.inactivity_timeout + Duration::from_secs(10)), keepalive_interval: config.store.ssh.keepalive_interval, methods: get_allowed_auth_methods(&services).await?, keys: russh_config_init.keys.clone(), diff --git a/warpgate-protocol-ssh/src/server/session.rs b/warpgate-protocol-ssh/src/server/session.rs index 85369b3e0..7b655e3c4 100644 --- a/warpgate-protocol-ssh/src/server/session.rs +++ b/warpgate-protocol-ssh/src/server/session.rs @@ -246,9 +246,22 @@ impl ServerSession { } })?; + let inactivity_timeout = services.config.lock().await.store.ssh.inactivity_timeout; + Ok(async move { - while let Some(event) = this.get_next_event().await { - this.handle_event(event).await?; + loop { + let next_event_fut = this.get_next_event(); + match tokio::time::timeout(inactivity_timeout, next_event_fut).await { + Ok(Some(event)) => this.handle_event(event).await?, + Ok(None) => break, + Err(_) => { + info!("Closing the session due to inactivity"); + let _ = this.emit_service_message("Closing the session due to inactivity"); + this.request_disconnect(); + this.disconnect_server().await; + break; + } + } } debug!("No more events"); Ok::<_, anyhow::Error>(()) From 3f04fba2a19ce1963b8fc8b223b2659d6c1729e6 Mon Sep 17 00:00:00 2001 From: Eugene Date: Thu, 18 Jun 2026 18:07:29 +0200 Subject: [PATCH 202/556] fixed #947 - configurable advertised MySQL server version (#2083) --- config-schema.json | 6 ++++++ warpgate-common/src/config/defaults.rs | 7 +++++++ warpgate-common/src/config/mod.rs | 11 +++++++++-- warpgate-protocol-mysql/src/session.rs | 7 ++++++- 4 files changed, 28 insertions(+), 3 deletions(-) diff --git a/config-schema.json b/config-schema.json index 17e3cf669..ebe4648ae 100644 --- a/config-schema.json +++ b/config-schema.json @@ -52,6 +52,7 @@ "mysql": { "$ref": "#/$defs/MySqlConfig", "default": { + "advertised_version": "8.0.3-Warpgate", "certificate": "", "enable": false, "external_host": null, @@ -251,6 +252,11 @@ "MySqlConfig": { "type": "object", "properties": { + "advertised_version": { + "description": "The server version advertised to clients during the handshake.\nWe can't auto-match the target's version since the target is only known\nafter the handshake, but clients use it to pick a protocol dialect.", + "type": "string", + "default": "8.0.3-Warpgate" + }, "certificate": { "type": "string", "default": "" diff --git a/warpgate-common/src/config/defaults.rs b/warpgate-common/src/config/defaults.rs index f8c1cbd27..e2a7ef08f 100644 --- a/warpgate-common/src/config/defaults.rs +++ b/warpgate-common/src/config/defaults.rs @@ -49,6 +49,13 @@ pub fn _default_mysql_listen() -> ListenEndpoint { ListenEndpoint::from(SocketAddr::new(Ipv6Addr::UNSPECIFIED.into(), 33306)) } +#[inline] +pub fn _default_mysql_advertised_version() -> String { + // Has to be >= 8.0.3 to stop Connector/J from probing + // query cache variables that no longer exist (#947) + "8.0.3-Warpgate".into() +} + #[inline] pub fn _default_postgres_listen() -> ListenEndpoint { ListenEndpoint::from(SocketAddr::new(Ipv6Addr::UNSPECIFIED.into(), 55432)) diff --git a/warpgate-common/src/config/mod.rs b/warpgate-common/src/config/mod.rs index 6ee63a4ab..9c0435f62 100644 --- a/warpgate-common/src/config/mod.rs +++ b/warpgate-common/src/config/mod.rs @@ -7,8 +7,8 @@ use std::time::Duration; use defaults::{ _default_audit_retention, _default_cookie_max_age, _default_database_url, _default_false, - _default_http_listen, _default_kubernetes_listen, _default_mysql_listen, - _default_postgres_listen, _default_recordings_path, _default_retention, + _default_http_listen, _default_kubernetes_listen, _default_mysql_advertised_version, + _default_mysql_listen, _default_postgres_listen, _default_recordings_path, _default_retention, _default_session_max_age, _default_ssh_inactivity_timeout, _default_ssh_keys_path, _default_ssh_listen, }; @@ -430,6 +430,12 @@ pub struct MySqlConfig { #[serde(default)] pub key: String, + + /// The server version advertised to clients during the handshake. + /// We can't auto-match the target's version since the target is only known + /// after the handshake, but clients use it to pick a protocol dialect. + #[serde(default = "_default_mysql_advertised_version")] + pub advertised_version: String, } impl Default for MySqlConfig { @@ -441,6 +447,7 @@ impl Default for MySqlConfig { external_host: None, certificate: "".into(), key: "".into(), + advertised_version: _default_mysql_advertised_version(), } } } diff --git a/warpgate-protocol-mysql/src/session.rs b/warpgate-protocol-mysql/src/session.rs index 0f4d7f405..9bf2b2153 100644 --- a/warpgate-protocol-mysql/src/session.rs +++ b/warpgate-protocol-mysql/src/session.rs @@ -95,9 +95,14 @@ impl MySqlSession { let challenge_2 = challenge_1.split_off(8); let challenge_chain = challenge_1.freeze().chain(challenge_2.freeze()); + let advertised_version = { + let config = self.services.config.lock().await; + config.store.mysql.advertised_version.clone() + }; + let handshake = Handshake { protocol_version: 10, - server_version: "8.0.0-Warpgate".to_owned(), + server_version: advertised_version, connection_id: 1, auth_plugin_data: challenge_chain, server_capabilities: self.capabilities, From bfca77d72a4150a7d177e8fe0eb84357bf2badcc Mon Sep 17 00:00:00 2001 From: Eugene Date: Thu, 18 Jun 2026 23:24:06 +0200 Subject: [PATCH 203/556] fixed #2065 - rsync/scp/Ansible hang: early channel data dropped (#2087) --- warpgate-protocol-ssh/src/server/session.rs | 9 ++++++++- 1 file changed, 8 insertions(+), 1 deletion(-) diff --git a/warpgate-protocol-ssh/src/server/session.rs b/warpgate-protocol-ssh/src/server/session.rs index 7b655e3c4..508480628 100644 --- a/warpgate-protocol-ssh/src/server/session.rs +++ b/warpgate-protocol-ssh/src/server/session.rs @@ -1508,7 +1508,14 @@ impl ServerSession { .await; } - if self.rc_state != RCState::Connected { + // While the target selection menu is open, keystrokes drive the menu + // (handled above) and there's no target to forward them to. + // Otherwise forward the data even before the target connection is + // established: the remote client buffers channel operations and + // replays them in order once connected, so early stdin (e.g. rsync, + // scp or Ansible pipelining payloads sent right after the exec + // request) must not be dropped (#2065). + if matches!(self.target, TargetSelection::Menu) { return Ok(()); } From 12425b6eab761c4f00822cef41fb54f693aadf91 Mon Sep 17 00:00:00 2001 From: Eugene Date: Thu, 18 Jun 2026 23:55:32 +0200 Subject: [PATCH 204/556] free disk space before tests --- .github/workflows/test.yml | 8 ++++++++ 1 file changed, 8 insertions(+) diff --git a/.github/workflows/test.yml b/.github/workflows/test.yml index d5625faad..ff09b819a 100644 --- a/.github/workflows/test.yml +++ b/.github/workflows/test.yml @@ -13,6 +13,14 @@ jobs: with: submodules: recursive + # Github runner ocasionally runs out of space causing flakiness + - name: Free disk space + uses: jlumbroso/free-disk-space@54081f138730dfa15788a46383842cd2f914a1be + with: + tool-cache: false + large-packages: false + docker-images: false + - uses: actions/setup-node@53b83947a5a98c8d113130e565377fae1a50d02f with: node-version: 24 From 3c148024039aa2e4240ac8b74f44611aae47308d Mon Sep 17 00:00:00 2001 From: Eugene Date: Thu, 18 Jun 2026 23:55:50 +0200 Subject: [PATCH 205/556] bump version to 0.25.5 --- bumpver.toml | 2 +- helm/warpgate/Chart.yaml | 2 +- helm/warpgate/values.yaml | 2 +- warpgate-admin/Cargo.toml | 2 +- warpgate-ca/Cargo.toml | 2 +- warpgate-common-http/Cargo.toml | 2 +- warpgate-common/Cargo.toml | 2 +- warpgate-core/Cargo.toml | 2 +- warpgate-database-protocols/Cargo.toml | 2 +- warpgate-db-entities/Cargo.toml | 2 +- warpgate-db-migrations/Cargo.toml | 2 +- warpgate-ldap/Cargo.toml | 2 +- warpgate-protocol-http/Cargo.toml | 2 +- warpgate-protocol-kubernetes/Cargo.toml | 2 +- warpgate-protocol-mysql/Cargo.toml | 2 +- warpgate-protocol-postgres/Cargo.toml | 2 +- warpgate-protocol-ssh/Cargo.toml | 2 +- warpgate-sso/Cargo.toml | 2 +- warpgate-tls/Cargo.toml | 2 +- warpgate-web/Cargo.toml | 2 +- warpgate/Cargo.toml | 2 +- 21 files changed, 21 insertions(+), 21 deletions(-) diff --git a/bumpver.toml b/bumpver.toml index 0a4c88a7c..3cf9c0161 100644 --- a/bumpver.toml +++ b/bumpver.toml @@ -1,5 +1,5 @@ [bumpver] -current_version = "0.25.4" +current_version = "0.25.5" version_pattern = "MAJOR.MINOR.PATCH[-TAG[.INC0]]" commit = true tag = false diff --git a/helm/warpgate/Chart.yaml b/helm/warpgate/Chart.yaml index 15d846910..5c5d6d532 100644 --- a/helm/warpgate/Chart.yaml +++ b/helm/warpgate/Chart.yaml @@ -22,4 +22,4 @@ version: 0.0.2 # incremented each time you make changes to the application. Versions are not expected to # follow Semantic Versioning. They should reflect the version the application is using. # It is recommended to use it with quotes. -appVersion: "0.25.4" +appVersion: "0.25.5" diff --git a/helm/warpgate/values.yaml b/helm/warpgate/values.yaml index 3b3c9f0c0..9581f7e95 100644 --- a/helm/warpgate/values.yaml +++ b/helm/warpgate/values.yaml @@ -5,7 +5,7 @@ replicaCount: 1 image: repository: ghcr.io/warp-tech/warpgate pullPolicy: IfNotPresent - tag: "0.25.4" + tag: "0.25.5" # References to Kubernetes secrets for pulling images (if using a private registry) imagePullSecrets: [] diff --git a/warpgate-admin/Cargo.toml b/warpgate-admin/Cargo.toml index 2d5629ac3..920fa7128 100644 --- a/warpgate-admin/Cargo.toml +++ b/warpgate-admin/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "warpgate-admin" -version = "0.25.4" +version = "0.25.5" edition = "2024" license = "Apache-2.0" publish = false diff --git a/warpgate-ca/Cargo.toml b/warpgate-ca/Cargo.toml index 66f908f50..bca8a6f1a 100644 --- a/warpgate-ca/Cargo.toml +++ b/warpgate-ca/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "warpgate-ca" -version = "0.25.4" +version = "0.25.5" edition = "2024" license = "Apache-2.0" publish = false diff --git a/warpgate-common-http/Cargo.toml b/warpgate-common-http/Cargo.toml index 8ce4b9f2b..cdf8ce3af 100644 --- a/warpgate-common-http/Cargo.toml +++ b/warpgate-common-http/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "warpgate-common-http" -version = "0.25.4" +version = "0.25.5" edition = "2024" license = "Apache-2.0" publish = false diff --git a/warpgate-common/Cargo.toml b/warpgate-common/Cargo.toml index 86ac33a2a..799380cc5 100644 --- a/warpgate-common/Cargo.toml +++ b/warpgate-common/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "warpgate-common" -version = "0.25.4" +version = "0.25.5" edition = "2024" license = "Apache-2.0" publish = false diff --git a/warpgate-core/Cargo.toml b/warpgate-core/Cargo.toml index da5447a9f..4b2986716 100644 --- a/warpgate-core/Cargo.toml +++ b/warpgate-core/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "warpgate-core" -version = "0.25.4" +version = "0.25.5" edition = "2024" license = "Apache-2.0" publish = false diff --git a/warpgate-database-protocols/Cargo.toml b/warpgate-database-protocols/Cargo.toml index 43a4072ac..57dec1b92 100644 --- a/warpgate-database-protocols/Cargo.toml +++ b/warpgate-database-protocols/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "warpgate-database-protocols" -version = "0.25.4" +version = "0.25.5" description = "Core of SQLx, the rust SQL toolkit. Just the database protocol parts." authors = [ "Ryan Leckey ", diff --git a/warpgate-db-entities/Cargo.toml b/warpgate-db-entities/Cargo.toml index 8554b22e9..6bcc10238 100644 --- a/warpgate-db-entities/Cargo.toml +++ b/warpgate-db-entities/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "warpgate-db-entities" -version = "0.25.4" +version = "0.25.5" edition = "2024" license = "Apache-2.0" publish = false diff --git a/warpgate-db-migrations/Cargo.toml b/warpgate-db-migrations/Cargo.toml index 9706f4971..15faaca4d 100644 --- a/warpgate-db-migrations/Cargo.toml +++ b/warpgate-db-migrations/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "warpgate-db-migrations" -version = "0.25.4" +version = "0.25.5" edition = "2024" license = "Apache-2.0" publish = false diff --git a/warpgate-ldap/Cargo.toml b/warpgate-ldap/Cargo.toml index 7cfe697a1..618161ece 100644 --- a/warpgate-ldap/Cargo.toml +++ b/warpgate-ldap/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "warpgate-ldap" -version = "0.25.4" +version = "0.25.5" edition = "2024" license = "Apache-2.0" publish = false diff --git a/warpgate-protocol-http/Cargo.toml b/warpgate-protocol-http/Cargo.toml index e71165351..d7e14ee98 100644 --- a/warpgate-protocol-http/Cargo.toml +++ b/warpgate-protocol-http/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "warpgate-protocol-http" -version = "0.25.4" +version = "0.25.5" edition = "2024" license = "Apache-2.0" publish = false diff --git a/warpgate-protocol-kubernetes/Cargo.toml b/warpgate-protocol-kubernetes/Cargo.toml index 8adb95d8e..593b4a6ee 100644 --- a/warpgate-protocol-kubernetes/Cargo.toml +++ b/warpgate-protocol-kubernetes/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "warpgate-protocol-kubernetes" -version = "0.25.4" +version = "0.25.5" edition = "2024" license = "Apache-2.0" publish = false diff --git a/warpgate-protocol-mysql/Cargo.toml b/warpgate-protocol-mysql/Cargo.toml index 4f8aef000..757cc7515 100644 --- a/warpgate-protocol-mysql/Cargo.toml +++ b/warpgate-protocol-mysql/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "warpgate-protocol-mysql" -version = "0.25.4" +version = "0.25.5" edition = "2024" license = "Apache-2.0" publish = false diff --git a/warpgate-protocol-postgres/Cargo.toml b/warpgate-protocol-postgres/Cargo.toml index 5782def06..a2a74625c 100644 --- a/warpgate-protocol-postgres/Cargo.toml +++ b/warpgate-protocol-postgres/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "warpgate-protocol-postgres" -version = "0.25.4" +version = "0.25.5" edition = "2024" license = "Apache-2.0" publish = false diff --git a/warpgate-protocol-ssh/Cargo.toml b/warpgate-protocol-ssh/Cargo.toml index 5bff7fc79..3401eeb9c 100644 --- a/warpgate-protocol-ssh/Cargo.toml +++ b/warpgate-protocol-ssh/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "warpgate-protocol-ssh" -version = "0.25.4" +version = "0.25.5" edition = "2024" license = "Apache-2.0" publish = false diff --git a/warpgate-sso/Cargo.toml b/warpgate-sso/Cargo.toml index 1b179f627..176951d15 100644 --- a/warpgate-sso/Cargo.toml +++ b/warpgate-sso/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "warpgate-sso" -version = "0.25.4" +version = "0.25.5" edition = "2024" license = "Apache-2.0" publish = false diff --git a/warpgate-tls/Cargo.toml b/warpgate-tls/Cargo.toml index 38c48e117..043db1e67 100644 --- a/warpgate-tls/Cargo.toml +++ b/warpgate-tls/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "warpgate-tls" -version = "0.25.4" +version = "0.25.5" edition = "2024" license = "Apache-2.0" publish = false diff --git a/warpgate-web/Cargo.toml b/warpgate-web/Cargo.toml index dc641b72d..9d023632f 100644 --- a/warpgate-web/Cargo.toml +++ b/warpgate-web/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "warpgate-web" -version = "0.25.4" +version = "0.25.5" edition = "2024" license = "Apache-2.0" diff --git a/warpgate/Cargo.toml b/warpgate/Cargo.toml index 815e4c7c9..0c6e0a77b 100644 --- a/warpgate/Cargo.toml +++ b/warpgate/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "warpgate" -version = "0.25.4" +version = "0.25.5" edition = "2024" license = "Apache-2.0" publish = false From 9bb0b2dc8b45405146d37f690cfbc4a2eb4b4516 Mon Sep 17 00:00:00 2001 From: Eugene Date: Thu, 18 Jun 2026 23:56:05 +0200 Subject: [PATCH 206/556] Update Cargo.lock --- Cargo.lock | 36 ++++++++++++++++++------------------ 1 file changed, 18 insertions(+), 18 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index 4971aa317..c8c0089aa 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -7780,7 +7780,7 @@ dependencies = [ [[package]] name = "warpgate" -version = "0.25.4" +version = "0.25.5" dependencies = [ "anyhow", "async-trait", @@ -7823,7 +7823,7 @@ dependencies = [ [[package]] name = "warpgate-admin" -version = "0.25.4" +version = "0.25.5" dependencies = [ "anyhow", "async-trait", @@ -7883,7 +7883,7 @@ dependencies = [ [[package]] name = "warpgate-ca" -version = "0.25.4" +version = "0.25.5" dependencies = [ "aws-lc-rs", "bytes", @@ -7904,7 +7904,7 @@ dependencies = [ [[package]] name = "warpgate-common" -version = "0.25.4" +version = "0.25.5" dependencies = [ "anyhow", "argon2 0.5.3", @@ -7957,7 +7957,7 @@ dependencies = [ [[package]] name = "warpgate-common-http" -version = "0.25.4" +version = "0.25.5" dependencies = [ "poem", "poem-openapi", @@ -7972,7 +7972,7 @@ dependencies = [ [[package]] name = "warpgate-core" -version = "0.25.4" +version = "0.25.5" dependencies = [ "anyhow", "argon2 0.5.3", @@ -8017,7 +8017,7 @@ dependencies = [ [[package]] name = "warpgate-database-protocols" -version = "0.25.4" +version = "0.25.5" dependencies = [ "bitflags 2.13.0", "bytes", @@ -8030,7 +8030,7 @@ dependencies = [ [[package]] name = "warpgate-db-entities" -version = "0.25.4" +version = "0.25.5" dependencies = [ "bytes", "ipnet", @@ -8049,7 +8049,7 @@ dependencies = [ [[package]] name = "warpgate-db-migrations" -version = "0.25.4" +version = "0.25.5" dependencies = [ "data-encoding", "regex", @@ -8067,7 +8067,7 @@ dependencies = [ [[package]] name = "warpgate-ldap" -version = "0.25.4" +version = "0.25.5" dependencies = [ "anyhow", "ldap3", @@ -8083,7 +8083,7 @@ dependencies = [ [[package]] name = "warpgate-protocol-http" -version = "0.25.4" +version = "0.25.5" dependencies = [ "anyhow", "async-trait", @@ -8122,7 +8122,7 @@ dependencies = [ [[package]] name = "warpgate-protocol-kubernetes" -version = "0.25.4" +version = "0.25.5" dependencies = [ "anyhow", "async-trait", @@ -8161,7 +8161,7 @@ dependencies = [ [[package]] name = "warpgate-protocol-mysql" -version = "0.25.4" +version = "0.25.5" dependencies = [ "anyhow", "async-trait", @@ -8189,7 +8189,7 @@ dependencies = [ [[package]] name = "warpgate-protocol-postgres" -version = "0.25.4" +version = "0.25.5" dependencies = [ "anyhow", "async-trait", @@ -8216,7 +8216,7 @@ dependencies = [ [[package]] name = "warpgate-protocol-ssh" -version = "0.25.4" +version = "0.25.5" dependencies = [ "anyhow", "async-trait", @@ -8249,7 +8249,7 @@ dependencies = [ [[package]] name = "warpgate-sso" -version = "0.25.4" +version = "0.25.5" dependencies = [ "bytes", "data-encoding", @@ -8269,7 +8269,7 @@ dependencies = [ [[package]] name = "warpgate-tls" -version = "0.25.4" +version = "0.25.5" dependencies = [ "bytes", "poem", @@ -8290,7 +8290,7 @@ dependencies = [ [[package]] name = "warpgate-web" -version = "0.25.4" +version = "0.25.5" dependencies = [ "rust-embed", "serde", From 21f6efe4b27e348154c870ae1223ef31cfcc4113 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Fri, 19 Jun 2026 00:09:35 +0200 Subject: [PATCH 207/556] Bump debian from bullseye-20260518 to bullseye-20260610 in /docker (#2053) Signed-off-by: dependabot[bot] --- docker/Dockerfile | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docker/Dockerfile b/docker/Dockerfile index 3bbff8e2f..a4f9c9d17 100644 --- a/docker/Dockerfile +++ b/docker/Dockerfile @@ -26,7 +26,7 @@ RUN just npm ci \ && just npm run build \ && cargo build --features mysql,postgres --release -FROM debian:bullseye-20260518@sha256:aeec37aebc55ca5cc6fcfb8d5f6ae2fd43d5017ad849e6e2fdb5325d61e144db +FROM debian:bullseye-20260610@sha256:68cf0d859b046494f3c4288171bc477580e424f981d08f2a77742b982c32a38f LABEL maintainer=heywoodlh ARG USER_ID=1000 From eab0548f018d95b5f96f8e913527000e05ed93a2 Mon Sep 17 00:00:00 2001 From: Eugene Date: Fri, 19 Jun 2026 00:37:23 +0200 Subject: [PATCH 208/556] HTML escaping --- Cargo.lock | 16 +++++++ Cargo.toml | 1 + warpgate-protocol-http/Cargo.toml | 1 + .../src/api/sso_provider_list.rs | 43 ++++++++++++++++++- warpgate-protocol-http/src/error.rs | 1 + 5 files changed, 61 insertions(+), 1 deletion(-) diff --git a/Cargo.lock b/Cargo.lock index c8c0089aa..f9cc5e8c0 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -2809,6 +2809,15 @@ dependencies = [ "windows-sys 0.61.2", ] +[[package]] +name = "html-escape" +version = "0.2.13" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6d1ad449764d627e22bfd7cd5e8868264fc9236e07c752972b4080cd351cb476" +dependencies = [ + "utf8-width", +] + [[package]] name = "http" version = "0.2.12" @@ -7701,6 +7710,12 @@ version = "0.7.6" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "09cc8ee72d2a9becf2f2febe0205bbed8fc6615b7cb429ad062dc7b7ddd036a9" +[[package]] +name = "utf8-width" +version = "0.1.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1292c0d970b54115d14f2492fe0170adf21d68a1de108eebc51c1df4f346a091" + [[package]] name = "utf8_iter" version = "1.0.4" @@ -8091,6 +8106,7 @@ dependencies = [ "data-encoding", "delegate", "futures", + "html-escape", "http 1.4.2", "percent-encoding", "poem", diff --git a/Cargo.toml b/Cargo.toml index 55c8b8006..5503a0cd7 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -74,6 +74,7 @@ poem = { version = "3.1", features = [ "server", ], default-features = false } hex = { version = "0.4", default-features = false } +html-escape = { version = "0.2", default-features = false } poem-openapi = { version = "5.1", features = [ "stoplight-elements", "uuid", diff --git a/warpgate-protocol-http/Cargo.toml b/warpgate-protocol-http/Cargo.toml index d7e14ee98..8cca82871 100644 --- a/warpgate-protocol-http/Cargo.toml +++ b/warpgate-protocol-http/Cargo.toml @@ -12,6 +12,7 @@ cookie = { version = "0.18", default-features = false } data-encoding.workspace = true delegate.workspace = true futures.workspace = true +html-escape.workspace = true http.workspace = true percent-encoding = { version = "2.1", default-features = false } poem.workspace = true diff --git a/warpgate-protocol-http/src/api/sso_provider_list.rs b/warpgate-protocol-http/src/api/sso_provider_list.rs index df88f1818..f1b8685c9 100644 --- a/warpgate-protocol-http/src/api/sso_provider_list.rs +++ b/warpgate-protocol-http/src/api/sso_provider_list.rs @@ -89,6 +89,19 @@ fn make_redirect_url(err: &str) -> String { format!("/@warpgate?login_error={err}") } +/// Only relative paths and absolute `http(s)` URLs are accepted as post-login +/// redirect targets. This rejects schemes such as `javascript:` or `data:` and +/// protocol-relative `//host` URLs. +fn is_safe_redirect_target(next: &str) -> bool { + if let Some(rest) = next.strip_prefix('/') { + // Relative path, but not protocol-relative ("//host") + return !rest.starts_with('/'); + } + url::Url::parse(next) + .as_ref() + .is_ok_and(|v| matches!(v.scheme(), "http" | "https")) +} + #[OpenApi] impl Api { #[oai( @@ -167,6 +180,8 @@ impl Api { .await? .unwrap_or_else(|x| make_redirect_url(&x)); let serialized_url = serde_json::to_string(&url)?; + let attr_url = html_escape::encode_double_quoted_attribute(&url); + let text_url = html_escape::encode_text(&url); Ok(ReturnToSsoPostResponse::Redirect( poem_openapi::payload::Html(format!( "\n @@ -175,7 +190,7 @@ impl Api { location.href = {serialized_url}; - Redirecting to {url}... + Redirecting to {text_url}... " @@ -410,6 +425,7 @@ impl Api { let mut next_url = context .next_url .as_deref() + .filter(|next| is_safe_redirect_target(next)) .unwrap_or("/@warpgate#/login") .to_owned(); @@ -462,3 +478,28 @@ impl Api { }))) } } + +#[cfg(test)] +mod tests { + use super::is_safe_redirect_target; + + #[test] + fn accepts_relative_paths() { + assert!(is_safe_redirect_target("/@warpgate#/login")); + assert!(is_safe_redirect_target("/foo/bar?x=1")); + } + + #[test] + fn accepts_http_and_https_urls() { + assert!(is_safe_redirect_target("https://example.com/path")); + assert!(is_safe_redirect_target("http://example.com")); + } + + #[test] + fn rejects_dangerous_schemes_and_protocol_relative() { + assert!(!is_safe_redirect_target("javascript:alert(1)")); + assert!(!is_safe_redirect_target("data:text/html, + +
+
+

login protection

+
+ +{#if error} + { error = undefined }}> + {error} + + +{/if} + +{#if loading && !status} + +{:else} + {#if status} +
+ 0 ? 'danger' : undefined} + value={status.blockedIpCount} + label="blocked IPs" + /> + 0 ? 'warning' : undefined} + value={status.lockedUserCount} + label="locked users" + /> + + +
+ {/if} + + {#if blockedIps && (blockedIps.length > 0)} +
+
Blocked IPs
+ {blockedIps.length} +
+
+ {#each blockedIps as ip (ip.ipAddress)} +
+
+
+ {ip.ipAddress} + + Block #{ip.blockCount} · expires + +
+ unblockIp(ip.ipAddress)}>Unblock +
+
+ {/each} +
+ {/if} + + {#if lockedUsers && (lockedUsers.length > 0)} +
+
Locked users
+ {lockedUsers.length} +
+
+ {#each lockedUsers as user (user.username)} +
+
+
+ {user.username} + + {#if user.expiresAt} + expires + {:else} + manual unlock required + {/if} + +
+ unlockUser(user.username)}>Unlock +
+
+ {/each} +
+ {/if} +{/if} +
+ + diff --git a/warpgate-web/src/admin/config/Parameters.svelte b/warpgate-web/src/admin/config/Parameters.svelte index 3c9fb5c38..ae18feb89 100644 --- a/warpgate-web/src/admin/config/Parameters.svelte +++ b/warpgate-web/src/admin/config/Parameters.svelte @@ -1,18 +1,35 @@ @@ -35,8 +66,17 @@
+ {#if updateError} + { updateError = undefined }}>{updateError} + {/if} {#if parameters} +
{ e.preventDefault(); save() }} + >
@@ -66,7 +102,6 @@ onchange={e => { const v = parseInt(e.currentTarget.value) parameters!.passwordPolicy.minLength = isNaN(v) ? 0 : Math.max(0, v) - update() }} /> @@ -78,11 +113,7 @@ id="requireUppercase" class="mb-0 me-2" type="switch" - on:change={() => { - parameters!.passwordPolicy.requireUppercase = !parameters!.passwordPolicy.requireUppercase - update() - }} - checked={parameters.passwordPolicy.requireUppercase} /> + bind:checked={parameters.passwordPolicy.requireUppercase} />
Require uppercase letter
@@ -138,7 +157,7 @@ + change={refreshValidity} /> @@ -153,11 +172,7 @@ id="sshClientAuthPublickey" class="mb-0 me-2" type="switch" - on:change={() => { - parameters!.sshClientAuthPublickey = !parameters!.sshClientAuthPublickey - update() - }} - checked={parameters.sshClientAuthPublickey} /> + bind:checked={parameters.sshClientAuthPublickey} />
Public key authentication
@@ -205,11 +213,7 @@ id="recordScp" class="mb-0 me-2" type="switch" - on:change={() => { - parameters!.recordScp = !parameters!.recordScp - update() - }} - checked={parameters.recordScp} /> + bind:checked={parameters.recordScp} />
Record legacy SCP transfers
@@ -226,11 +230,7 @@ id="ticketSelfServiceEnabled" class="mb-0 me-2" type="switch" - on:change={() => { - parameters!.ticketSelfServiceEnabled = !parameters!.ticketSelfServiceEnabled - update() - }} - checked={parameters.ticketSelfServiceEnabled} /> + bind:checked={parameters.ticketSelfServiceEnabled} />
Allow users to request tickets
@@ -246,11 +246,7 @@ id="ticketAutoApproveExistingAccess" class="mb-0 me-2" type="switch" - on:change={() => { - parameters!.ticketAutoApproveExistingAccess = !parameters!.ticketAutoApproveExistingAccess - update() - }} - checked={parameters.ticketAutoApproveExistingAccess} /> + bind:checked={parameters.ticketAutoApproveExistingAccess} />
Auto-approve when user already has role-based access
@@ -262,11 +258,7 @@ id="ticketRequireDescription" class="mb-0 me-2" type="switch" - on:change={() => { - parameters!.ticketRequireDescription = !parameters!.ticketRequireDescription - update() - }} - checked={parameters.ticketRequireDescription} /> + bind:checked={parameters.ticketRequireDescription} />
Require description on ticket requests
@@ -278,11 +270,7 @@ id="ticketRequestShowAllTargets" class="mb-0 me-2" type="switch" - on:change={() => { - parameters!.ticketRequestShowAllTargets = !parameters!.ticketRequestShowAllTargets - update() - }} - checked={parameters.ticketRequestShowAllTargets} /> + bind:checked={parameters.ticketRequestShowAllTargets} />
Show all targets in ticket request form
@@ -294,7 +282,7 @@ type="text" class="form-control" placeholder="e.g. 8h, 30m, 1d" - use:humantimeDuration={{ seconds: parameters.ticketMaxDurationSeconds, onChange: v => { parameters!.ticketMaxDurationSeconds = v; update() } }} + use:humantimeDuration={{ seconds: parameters.ticketMaxDurationSeconds, onChange: v => { parameters!.ticketMaxDurationSeconds = v } }} /> Global default. Can be overridden per target. Examples: 30m, 8h, 1d, 2h30m. @@ -310,7 +298,6 @@ onchange={e => { const v = parseInt(e.currentTarget.value) parameters!.ticketMaxUses = isNaN(v) ? undefined : v - update() }} /> @@ -323,7 +310,7 @@ type="text" class="form-control" placeholder="e.g. 8h, 30m, 1d" - use:humantimeDuration={{ seconds: parameters.maxApiTokenDurationSeconds, onChange: v => { parameters!.maxApiTokenDurationSeconds = v; update() } }} + use:humantimeDuration={{ seconds: parameters.maxApiTokenDurationSeconds, onChange: v => { parameters!.maxApiTokenDurationSeconds = v } }} /> @@ -334,10 +321,7 @@ id="targetClickAction" class="form-select" value={parameters.targetClickAction ?? 'Connect'} - onchange={e => { - parameters!.targetClickAction = e.currentTarget.value as TargetClickAction - update() - }} + onchange={e => parameters!.targetClickAction = e.currentTarget.value as TargetClickAction} > @@ -352,11 +336,7 @@ id="showSessionMenu" class="mb-0 me-2" type="switch" - on:change={() => { - parameters!.showSessionMenu = !parameters!.showSessionMenu - update() - }} - checked={parameters.showSessionMenu} /> + bind:checked={parameters.showSessionMenu} />
Show HTTP session menu
@@ -374,11 +354,7 @@ id="minimizePasswordLogin" class="mb-0 me-2" type="switch" - on:change={() => { - parameters!.minimizePasswordLogin = !parameters!.minimizePasswordLogin - update() - }} - checked={parameters.minimizePasswordLogin} /> + bind:checked={parameters.minimizePasswordLogin} />
Minimize password login UI
@@ -386,8 +362,162 @@ {/if} + +
+ + + + Rate-limits IPs and locks accounts after repeated failed logins. When disabled, all settings below are preserved but not enforced. + + + {#if lpCapWarning} + {lpCapWarning} + {/if} + + {#if parameters.loginProtectionEnabled} +

IP rate-limit

+
+
+ + { parameters!.lpIpMaxAttempts = e.currentTarget.valueAsNumber }} /> + +
+
+ + { if (v != null) { parameters!.lpIpTimeWindowSeconds = v } } }} /> + +
+
+ + { if (v != null) { parameters!.lpIpBaseBlockDurationSeconds = v } } }} /> + +
+
+ + { parameters!.lpIpBlockDurationMultiplier = e.currentTarget.valueAsNumber }} /> + +
+
+ + { if (v != null) { parameters!.lpIpMaxBlockDurationSeconds = v } } }} /> + +
+
+ + { if (v != null) { parameters!.lpIpCooldownResetSeconds = v } } }} /> + +
+
+ + Each block is multiplier × the previous block duration, capped at the maximum. The repeat count resets only after the cooldown period of clean activity — not when a block expires. + + +

User lockout

+
+
+ + { parameters!.lpUserMaxAttempts = e.currentTarget.valueAsNumber }} /> + +
+
+ + { if (v != null) { parameters!.lpUserTimeWindowSeconds = v } } }} /> + +
+
+ + {#if parameters.lpUserAutoUnlock} + + { if (v != null) { parameters!.lpUserLockoutDurationSeconds = v } } }} /> + + {/if} + + + Recommended: keeps an attacker from locking out an admin account by spamming its username. IP blocking still applies to everyone. + + +

Data retention

+ + { if (v != null) { parameters!.loginProtectionRetentionSeconds = v } } }} /> + + + + Manage active blocks & lockouts on the Login protection page. + + {/if} +
+ + + + Save + + + {/if}
+ + diff --git a/warpgate-web/src/admin/lib/openapi-schema.json b/warpgate-web/src/admin/lib/openapi-schema.json index c8b664005..65eb8db30 100644 --- a/warpgate-web/src/admin/lib/openapi-schema.json +++ b/warpgate-web/src/admin/lib/openapi-schema.json @@ -2,7 +2,7 @@ "openapi": "3.0.0", "info": { "title": "Warpgate Web Admin", - "version": "v0.25.3-modified" + "version": "v0.25.5-40-g6c82ea58-modified" }, "servers": [ { @@ -3739,6 +3739,152 @@ "operationId": "check_ssh_host_key" } }, + "/login-protection/blocked-ips": { + "get": { + "summary": "List all currently blocked IPs.", + "responses": { + "200": { + "description": "", + "content": { + "application/json; charset=utf-8": { + "schema": { + "type": "array", + "items": { + "$ref": "#/components/schemas/BlockedIpInfo" + } + } + } + } + } + }, + "security": [ + { + "TokenSecurityScheme": [] + }, + { + "CookieSecurityScheme": [] + } + ], + "operationId": "list_blocked_ips" + }, + "delete": { + "summary": "Unblock an IP address.", + "description": "The IP is taken from the request body (rather than a path segment or\nquery parameter) to avoid encoding ambiguity with IPv6 addresses.", + "requestBody": { + "content": { + "application/json; charset=utf-8": { + "schema": { + "$ref": "#/components/schemas/UnblockIpRequest" + } + } + }, + "required": true + }, + "responses": { + "200": { + "description": "" + }, + "400": { + "description": "" + } + }, + "security": [ + { + "TokenSecurityScheme": [] + }, + { + "CookieSecurityScheme": [] + } + ], + "operationId": "unblock_ip" + } + }, + "/login-protection/locked-users": { + "get": { + "summary": "List all currently locked user accounts.", + "responses": { + "200": { + "description": "", + "content": { + "application/json; charset=utf-8": { + "schema": { + "type": "array", + "items": { + "$ref": "#/components/schemas/LockedUserInfo" + } + } + } + } + } + }, + "security": [ + { + "TokenSecurityScheme": [] + }, + { + "CookieSecurityScheme": [] + } + ], + "operationId": "list_locked_users" + } + }, + "/login-protection/locked-users/{username}": { + "delete": { + "summary": "Unlock a user account by username.", + "parameters": [ + { + "name": "username", + "schema": { + "type": "string" + }, + "in": "path", + "required": true, + "deprecated": false, + "explode": true + } + ], + "responses": { + "200": { + "description": "" + } + }, + "security": [ + { + "TokenSecurityScheme": [] + }, + { + "CookieSecurityScheme": [] + } + ], + "operationId": "unlock_user" + } + }, + "/login-protection/status": { + "get": { + "summary": "Get security status summary (blocked IPs, locked users, failure counts).", + "responses": { + "200": { + "description": "", + "content": { + "application/json; charset=utf-8": { + "schema": { + "$ref": "#/components/schemas/SecurityStatus" + } + } + } + } + }, + "security": [ + { + "TokenSecurityScheme": [] + }, + { + "CookieSecurityScheme": [] + } + ], + "operationId": "get_security_status" + } + }, "/users/{user_id}/credentials/certificates": { "get": { "parameters": [ @@ -4153,6 +4299,37 @@ } } }, + "BlockedIpInfo": { + "type": "object", + "title": "BlockedIpInfo", + "required": [ + "ip_address", + "blocked_at", + "expires_at", + "block_count", + "reason" + ], + "properties": { + "ip_address": { + "type": "string" + }, + "blocked_at": { + "type": "string", + "format": "date-time" + }, + "expires_at": { + "type": "string", + "format": "date-time" + }, + "block_count": { + "type": "integer", + "format": "int32" + }, + "reason": { + "type": "string" + } + } + }, "BootstrapThemeColor": { "type": "string", "enum": [ @@ -4865,6 +5042,31 @@ "SamAccountName" ] }, + "LockedUserInfo": { + "type": "object", + "title": "LockedUserInfo", + "required": [ + "username", + "locked_at", + "reason" + ], + "properties": { + "username": { + "type": "string" + }, + "locked_at": { + "type": "string", + "format": "date-time" + }, + "expires_at": { + "type": "string", + "format": "date-time" + }, + "reason": { + "type": "string" + } + } + }, "LogEntry": { "type": "object", "title": "LogEntry", @@ -4997,9 +5199,6 @@ "ParameterUpdate": { "type": "object", "title": "ParameterUpdate", - "required": [ - "allow_own_credential_management" - ], "properties": { "allow_own_credential_management": { "type": "boolean" @@ -5055,6 +5254,55 @@ }, "record_scp": { "type": "boolean" + }, + "login_protection_enabled": { + "type": "boolean" + }, + "login_protection_retention_seconds": { + "type": "integer", + "format": "int32" + }, + "lp_ip_max_attempts": { + "type": "integer", + "format": "int32" + }, + "lp_ip_time_window_seconds": { + "type": "integer", + "format": "int32" + }, + "lp_ip_base_block_duration_seconds": { + "type": "integer", + "format": "int32" + }, + "lp_ip_block_duration_multiplier": { + "type": "number", + "format": "double" + }, + "lp_ip_max_block_duration_seconds": { + "type": "integer", + "format": "int32" + }, + "lp_ip_cooldown_reset_seconds": { + "type": "integer", + "format": "int32" + }, + "lp_user_max_attempts": { + "type": "integer", + "format": "int32" + }, + "lp_user_time_window_seconds": { + "type": "integer", + "format": "int32" + }, + "lp_user_auto_unlock": { + "type": "boolean" + }, + "lp_user_lockout_duration_seconds": { + "type": "integer", + "format": "int32" + }, + "lp_user_exempt_admins": { + "type": "boolean" } } }, @@ -5074,7 +5322,20 @@ "target_click_action", "show_session_menu", "password_policy", - "record_scp" + "record_scp", + "login_protection_enabled", + "login_protection_retention_seconds", + "lp_ip_max_attempts", + "lp_ip_time_window_seconds", + "lp_ip_base_block_duration_seconds", + "lp_ip_block_duration_multiplier", + "lp_ip_max_block_duration_seconds", + "lp_ip_cooldown_reset_seconds", + "lp_user_max_attempts", + "lp_user_time_window_seconds", + "lp_user_auto_unlock", + "lp_user_lockout_duration_seconds", + "lp_user_exempt_admins" ], "properties": { "allow_own_credential_management": { @@ -5131,6 +5392,55 @@ }, "record_scp": { "type": "boolean" + }, + "login_protection_enabled": { + "type": "boolean" + }, + "login_protection_retention_seconds": { + "type": "integer", + "format": "int32" + }, + "lp_ip_max_attempts": { + "type": "integer", + "format": "int32" + }, + "lp_ip_time_window_seconds": { + "type": "integer", + "format": "int32" + }, + "lp_ip_base_block_duration_seconds": { + "type": "integer", + "format": "int32" + }, + "lp_ip_block_duration_multiplier": { + "type": "number", + "format": "double" + }, + "lp_ip_max_block_duration_seconds": { + "type": "integer", + "format": "int32" + }, + "lp_ip_cooldown_reset_seconds": { + "type": "integer", + "format": "int32" + }, + "lp_user_max_attempts": { + "type": "integer", + "format": "int32" + }, + "lp_user_time_window_seconds": { + "type": "integer", + "format": "int32" + }, + "lp_user_auto_unlock": { + "type": "boolean" + }, + "lp_user_lockout_duration_seconds": { + "type": "integer", + "format": "int32" + }, + "lp_user_exempt_admins": { + "type": "boolean" } } }, @@ -5407,6 +5717,34 @@ } ] }, + "SecurityStatus": { + "type": "object", + "title": "SecurityStatus", + "required": [ + "blocked_ip_count", + "locked_user_count", + "failed_attempts_last_hour", + "failed_attempts_last_24h" + ], + "properties": { + "blocked_ip_count": { + "type": "integer", + "format": "uint64" + }, + "locked_user_count": { + "type": "integer", + "format": "uint64" + }, + "failed_attempts_last_hour": { + "type": "integer", + "format": "uint64" + }, + "failed_attempts_last_24h": { + "type": "integer", + "format": "uint64" + } + } + }, "SessionSnapshot": { "type": "object", "title": "SessionSnapshot", @@ -6114,6 +6452,19 @@ "Required" ] }, + "UnblockIpRequest": { + "type": "object", + "title": "UnblockIpRequest", + "required": [ + "ip" + ], + "properties": { + "ip": { + "type": "string", + "description": "IP address to unblock (IPv4 or IPv6)." + } + } + }, "UpdateCertificateCredential": { "type": "object", "title": "UpdateCertificateCredential", diff --git a/warpgate-web/src/common/StatCard.svelte b/warpgate-web/src/common/StatCard.svelte new file mode 100644 index 000000000..76d527234 --- /dev/null +++ b/warpgate-web/src/common/StatCard.svelte @@ -0,0 +1,40 @@ + + +
+
{value}
+
{label}
+
+ + diff --git a/warpgate-web/src/gateway/lib/openapi-schema.json b/warpgate-web/src/gateway/lib/openapi-schema.json index bf157b68d..fc13e2647 100644 --- a/warpgate-web/src/gateway/lib/openapi-schema.json +++ b/warpgate-web/src/gateway/lib/openapi-schema.json @@ -2,7 +2,7 @@ "openapi": "3.0.0", "info": { "title": "Warpgate HTTP proxy", - "version": "v0.25.3-modified" + "version": "v0.25.5-37-g69c02d3f-modified" }, "servers": [ { @@ -1393,6 +1393,8 @@ "WebUserApprovalNeeded", "PublicKeyNeeded", "Success", + "IpBlocked", + "UserLocked", "IpRejected" ] }, From e15f250c8e3428fcd1d69a2e3fc3ca501aed798e Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Mon, 29 Jun 2026 14:34:48 +0200 Subject: [PATCH 227/556] Bump actions/checkout from 6.0.3 to 7.0.0 (#2088) Signed-off-by: dependabot[bot] --- .github/workflows/build.yml | 4 ++-- .github/workflows/cargo-deny.yml | 2 +- .github/workflows/check-schema-compatibility.yml | 4 ++-- .github/workflows/clippy.yml | 2 +- .github/workflows/codeql.yml | 2 +- .github/workflows/docker.yml | 2 +- .github/workflows/helm-publish.yaml | 4 ++-- .github/workflows/reprotest.yml | 2 +- .github/workflows/scorecard.yml | 2 +- .github/workflows/test.yml | 2 +- 10 files changed, 13 insertions(+), 13 deletions(-) diff --git a/.github/workflows/build.yml b/.github/workflows/build.yml index 690843f4f..824e3be15 100644 --- a/.github/workflows/build.yml +++ b/.github/workflows/build.yml @@ -45,7 +45,7 @@ jobs: sudo apt update sudo apt install -y libssl-dev pkg-config - - uses: actions/checkout@df4cb1c069e1874edd31b4311f1884172cec0e10 + - uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 with: submodules: recursive @@ -156,7 +156,7 @@ jobs: sudo apt update sudo apt install --no-install-recommends -y libssl-dev pkg-config - - uses: actions/checkout@df4cb1c069e1874edd31b4311f1884172cec0e10 + - uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 with: submodules: recursive diff --git a/.github/workflows/cargo-deny.yml b/.github/workflows/cargo-deny.yml index a70e622ee..4f212ff5e 100644 --- a/.github/workflows/cargo-deny.yml +++ b/.github/workflows/cargo-deny.yml @@ -9,7 +9,7 @@ jobs: name: cargo-deny runs-on: ubuntu-latest steps: - - uses: actions/checkout@df4cb1c069e1874edd31b4311f1884172cec0e10 + - uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 - name: Install cargo-deny run: cargo install --locked cargo-deny@0.18.9 diff --git a/.github/workflows/check-schema-compatibility.yml b/.github/workflows/check-schema-compatibility.yml index 89e802740..4969623cf 100644 --- a/.github/workflows/check-schema-compatibility.yml +++ b/.github/workflows/check-schema-compatibility.yml @@ -9,7 +9,7 @@ jobs: runs-on: ubuntu-latest steps: - name: Checkout PR branch - uses: actions/checkout@df4cb1c069e1874edd31b4311f1884172cec0e10 + uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 with: fetch-depth: 0 @@ -27,7 +27,7 @@ jobs: just openapi-all - name: Checkout main branch to compare - uses: actions/checkout@df4cb1c069e1874edd31b4311f1884172cec0e10 + uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 with: ref: main path: main-branch diff --git a/.github/workflows/clippy.yml b/.github/workflows/clippy.yml index 220b9d136..e26686e83 100644 --- a/.github/workflows/clippy.yml +++ b/.github/workflows/clippy.yml @@ -9,7 +9,7 @@ jobs: runs-on: ubuntu-latest steps: - name: Checkout PR branch - uses: actions/checkout@df4cb1c069e1874edd31b4311f1884172cec0e10 + uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 with: fetch-depth: 0 diff --git a/.github/workflows/codeql.yml b/.github/workflows/codeql.yml index cfec5ecf6..9636d4d3f 100644 --- a/.github/workflows/codeql.yml +++ b/.github/workflows/codeql.yml @@ -52,7 +52,7 @@ jobs: build-mode: none steps: - name: Checkout repository - uses: actions/checkout@df4cb1c069e1874edd31b4311f1884172cec0e10 + uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 # Add any setup steps before running the `github/codeql-action/init` action. # This includes steps like installing compilers or runtimes (`actions/setup-node` diff --git a/.github/workflows/docker.yml b/.github/workflows/docker.yml index 0790846a9..ebca64759 100644 --- a/.github/workflows/docker.yml +++ b/.github/workflows/docker.yml @@ -34,7 +34,7 @@ jobs: steps: - name: Checkout repository - uses: actions/checkout@df4cb1c069e1874edd31b4311f1884172cec0e10 + uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 with: submodules: recursive fetch-depth: 0 diff --git a/.github/workflows/helm-publish.yaml b/.github/workflows/helm-publish.yaml index fb4b95f0b..67599a0ca 100644 --- a/.github/workflows/helm-publish.yaml +++ b/.github/workflows/helm-publish.yaml @@ -20,7 +20,7 @@ jobs: runs-on: ubuntu-latest steps: - name: Checkout repository - uses: actions/checkout@df4cb1c069e1874edd31b4311f1884172cec0e10 + uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 - name: Set up Helm uses: azure/setup-helm@9bc31f4ebc9c6b171d7bfbaa5d006ae7abdb4310 # v5 @@ -42,7 +42,7 @@ jobs: steps: - name: Checkout repository - uses: actions/checkout@df4cb1c069e1874edd31b4311f1884172cec0e10 + uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 - name: Set up Helm uses: azure/setup-helm@9bc31f4ebc9c6b171d7bfbaa5d006ae7abdb4310 # v5 diff --git a/.github/workflows/reprotest.yml b/.github/workflows/reprotest.yml index 2e6621d98..4526a9335 100644 --- a/.github/workflows/reprotest.yml +++ b/.github/workflows/reprotest.yml @@ -19,7 +19,7 @@ jobs: curl --proto '=https' --tlsv1.2 -sSf https://sh.rustup.rs | sudo sh -s -- -y echo "/root/.cargo/bin" >> $GITHUB_PATH - - uses: actions/checkout@df4cb1c069e1874edd31b4311f1884172cec0e10 + - uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 with: submodules: recursive diff --git a/.github/workflows/scorecard.yml b/.github/workflows/scorecard.yml index 11e8644a2..11465597e 100644 --- a/.github/workflows/scorecard.yml +++ b/.github/workflows/scorecard.yml @@ -34,7 +34,7 @@ jobs: steps: - name: "Checkout code" - uses: actions/checkout@df4cb1c069e1874edd31b4311f1884172cec0e10 # v6.0.3 + uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 # v7.0.0 with: persist-credentials: false diff --git a/.github/workflows/test.yml b/.github/workflows/test.yml index ff09b819a..5b6feda30 100644 --- a/.github/workflows/test.yml +++ b/.github/workflows/test.yml @@ -9,7 +9,7 @@ jobs: Tests: runs-on: ubuntu-latest steps: - - uses: actions/checkout@df4cb1c069e1874edd31b4311f1884172cec0e10 + - uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 with: submodules: recursive From bb86bed7f542b517efa0fb7c1b7f607df42f0539 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Mon, 29 Jun 2026 14:34:56 +0200 Subject: [PATCH 228/556] Bump rust from `e12c121` to `7069898` in /docker (#2100) Signed-off-by: dependabot[bot] --- docker/Dockerfile | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docker/Dockerfile b/docker/Dockerfile index 705a7687c..d71718124 100644 --- a/docker/Dockerfile +++ b/docker/Dockerfile @@ -1,6 +1,6 @@ # syntax=docker/dockerfile:1.3-labs # hadolint global ignore=DL3008 -FROM rust:1.96.0-bullseye@sha256:e12c121572a8e557ce164e10755da72047be5b689edb5ae617a6a5d37683a0fc AS build +FROM rust:1.96.0-bullseye@sha256:7069898d5edfc11b0ba498ecefbcc5438f6390b3ce0be11a9750cf39cab7e02f AS build ENV DEBIAN_FRONTEND=noninteractive From b2bdcd09a0cf9659c947c7e87866c99f0bbbf796 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Mon, 29 Jun 2026 14:35:03 +0200 Subject: [PATCH 229/556] Bump softprops/action-gh-release from 3.0.0 to 3.0.1 (#2091) Signed-off-by: dependabot[bot] --- .github/workflows/build.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/build.yml b/.github/workflows/build.yml index 824e3be15..f558592a2 100644 --- a/.github/workflows/build.yml +++ b/.github/workflows/build.yml @@ -138,7 +138,7 @@ jobs: mv cdx.xml dist/warpgate-${{ env.GITHUB_REF_SLUG }}-${{ matrix.arch }}.cdx.xml - name: Upload release - uses: softprops/action-gh-release@b4309332981a82ec1c5618f44dd2e27cc8bfbfda + uses: softprops/action-gh-release@718ea10b132b3b2eba29c1007bb80653f286566b if: startsWith(github.ref, 'refs/tags/v') with: draft: true From 82041787b1b34cc1c1543348cf0f49c889a65e62 Mon Sep 17 00:00:00 2001 From: Eugene Date: Mon, 29 Jun 2026 20:03:08 +0200 Subject: [PATCH 230/556] fixed #2027 - case-insensitive username comparison for web approval (#2117) --- warpgate-common/src/helpers/mod.rs | 1 + warpgate-common/src/helpers/username.rs | 20 ++++++++++++++++++++ warpgate-core/src/auth_state_store.rs | 5 +++-- warpgate-protocol-http/src/api/auth.rs | 5 +++-- warpgate-protocol-http/src/common.rs | 3 ++- warpgate-protocol-http/src/proxy.rs | 6 +++--- warpgate-protocol-ssh/src/server/session.rs | 21 ++++++++++++--------- 7 files changed, 44 insertions(+), 17 deletions(-) create mode 100644 warpgate-common/src/helpers/username.rs diff --git a/warpgate-common/src/helpers/mod.rs b/warpgate-common/src/helpers/mod.rs index 5e05662ba..fe72acb10 100644 --- a/warpgate-common/src/helpers/mod.rs +++ b/warpgate-common/src/helpers/mod.rs @@ -9,4 +9,5 @@ pub mod password_policy; pub mod rng; pub mod serde_base64; pub mod serde_base64_secret; +pub mod username; pub mod websocket; diff --git a/warpgate-common/src/helpers/username.rs b/warpgate-common/src/helpers/username.rs new file mode 100644 index 000000000..58054bb3e --- /dev/null +++ b/warpgate-common/src/helpers/username.rs @@ -0,0 +1,20 @@ +pub fn username_eq_ci(a: &str, b: &str) -> bool { + a.to_lowercase() == b.to_lowercase() +} + +#[cfg(test)] +mod tests { + use super::username_eq_ci; + + #[test] + fn matches_regardless_of_case() { + assert!(username_eq_ci("Alice", "alice")); + assert!(username_eq_ci("ALICE", "alice")); + assert!(username_eq_ci("alice", "alice")); + } + + #[test] + fn rejects_different_names() { + assert!(!username_eq_ci("alice", "bob")); + } +} diff --git a/warpgate-core/src/auth_state_store.rs b/warpgate-core/src/auth_state_store.rs index 022a1a359..ffdf529eb 100644 --- a/warpgate-core/src/auth_state_store.rs +++ b/warpgate-core/src/auth_state_store.rs @@ -7,6 +7,7 @@ use tokio::sync::{Mutex, broadcast}; use uuid::Uuid; use warpgate_common::auth::{AuthResult, AuthState, CredentialKind}; use warpgate_common::helpers::ipnet::WarpgateIpNet; +use warpgate_common::helpers::username::username_eq_ci; use warpgate_common::{SessionId, WarpgateError}; use crate::{ConfigProvider, ConfigProviderEnum}; @@ -99,7 +100,7 @@ impl AuthStateStore { for auth in self.store.values() { { let inner = auth.0.lock().await; - if inner.user_info().username != username { + if !username_eq_ci(&inner.user_info().username, username) { continue; } let AuthResult::Need(need) = inner.verify() else { @@ -139,7 +140,7 @@ impl AuthStateStore { .list_users() .await? .iter() - .find(|u| u.username.to_lowercase() == username.to_lowercase()) + .find(|u| username_eq_ci(&u.username, username)) .cloned() else { return Err(WarpgateError::UserNotFound(username.into())); diff --git a/warpgate-protocol-http/src/api/auth.rs b/warpgate-protocol-http/src/api/auth.rs index b7b4db091..27d760dc8 100644 --- a/warpgate-protocol-http/src/api/auth.rs +++ b/warpgate-protocol-http/src/api/auth.rs @@ -16,6 +16,7 @@ use tracing::{error, warn}; use uuid::Uuid; use warpgate_admin::api::AnySecurityScheme; use warpgate_common::auth::{AuthCredential, AuthResult, AuthState, CredentialKind}; +use warpgate_common::helpers::username::username_eq_ci; use warpgate_common::{Secret, WarpgateError}; use warpgate_common_http::auth::{AuthenticatedRequestContext, UnauthenticatedRequestContext}; use warpgate_common_http::logging::get_client_ip; @@ -569,7 +570,7 @@ async fn get_foreign_auth_state( { let state = state_arc.lock().await; - if &state.user_info().username != username { + if !username_eq_ci(&state.user_info().username, username) { return None; } } @@ -630,7 +631,7 @@ pub async fn api_get_web_auth_requests_stream( let auth_state_store = auth_state_store.lock().await; if let Some(state) = auth_state_store.get(&id) { let state = state.lock().await; - if state.user_info().username == username { + if username_eq_ci(&state.user_info().username, &username) { sink.send(Message::Text(id.to_string())).await?; } } diff --git a/warpgate-protocol-http/src/common.rs b/warpgate-protocol-http/src/common.rs index 724b1a920..580af764b 100644 --- a/warpgate-protocol-http/src/common.rs +++ b/warpgate-protocol-http/src/common.rs @@ -14,6 +14,7 @@ use subtle::ConstantTimeEq; use tokio::sync::Mutex; use uuid::Uuid; use warpgate_common::auth::{AuthState, AuthStateUserInfo, CredentialKind}; +use warpgate_common::helpers::username::username_eq_ci; use warpgate_common::{ProtocolName, SessionId, WarpgateError}; use warpgate_common_http::auth::UnauthenticatedRequestContext; use warpgate_common_http::ext::construct_external_url; @@ -202,7 +203,7 @@ pub async fn get_or_create_auth_state_for_request( .context("Session not in request")?; if let Some(state) = get_auth_state_for_request(req, ctx).await? { - let existing_matched = state.lock().await.user_info().username == username; + let existing_matched = username_eq_ci(&state.lock().await.user_info().username, username); if existing_matched { return Ok(state); } diff --git a/warpgate-protocol-http/src/proxy.rs b/warpgate-protocol-http/src/proxy.rs index ce0e7770f..65e5baab6 100644 --- a/warpgate-protocol-http/src/proxy.rs +++ b/warpgate-protocol-http/src/proxy.rs @@ -7,12 +7,12 @@ use cookie::Cookie; use data_encoding::BASE64; use delegate::delegate; use futures::{StreamExt, TryStreamExt}; -use http::StatusCode; use http::header::HeaderName; use http::uri::{Authority, Scheme}; -use http::{HeaderValue, Uri}; +use http::{HeaderValue, StatusCode, Uri}; use poem::session::Session; -use poem::web::{Data, websocket::WebSocket}; +use poem::web::Data; +use poem::web::websocket::WebSocket; use poem::{Body, FromRequest, IntoResponse, Request, Response}; use tokio::sync::Mutex; use tokio_tungstenite::{Connector, connect_async_tls_with_config, tungstenite}; diff --git a/warpgate-protocol-ssh/src/server/session.rs b/warpgate-protocol-ssh/src/server/session.rs index 0222767c3..e0037047d 100644 --- a/warpgate-protocol-ssh/src/server/session.rs +++ b/warpgate-protocol-ssh/src/server/session.rs @@ -22,6 +22,7 @@ use warpgate_common::auth::{ AuthCredential, AuthResult, AuthSelector, AuthState, AuthStateUserInfo, CredentialKind, }; use warpgate_common::eventhub::{EventHub, EventSender, EventSubscription}; +use warpgate_common::helpers::username::username_eq_ci; use warpgate_common::{ Secret, SessionId, SshHostKeyVerificationMode, Target, TargetOptions, WarpgateError, }; @@ -295,15 +296,17 @@ impl ServerSession { async fn get_auth_state(&mut self, username: &str) -> Result>> { #[allow(clippy::unwrap_used)] if self.auth_state.is_none() - || self - .auth_state - .as_ref() - .unwrap() - .lock() - .await - .user_info() - .username - != username + || !username_eq_ci( + &self + .auth_state + .as_ref() + .unwrap() + .lock() + .await + .user_info() + .username, + username, + ) { let state = self .services From 9d3c853a4749ef62563ef235f0a27994815881e2 Mon Sep 17 00:00:00 2001 From: Eugene Date: Mon, 29 Jun 2026 20:37:13 +0200 Subject: [PATCH 231/556] fixed #2103 - do not auto-advance login state to SSO on invalid password (#2118) --- warpgate-protocol-http/src/api/auth.rs | 123 +++++++++++------- .../src/admin/lib/openapi-schema.json | 2 +- warpgate-web/src/gateway/Login.svelte | 17 ++- .../src/gateway/lib/openapi-schema.json | 9 +- 4 files changed, 95 insertions(+), 56 deletions(-) diff --git a/warpgate-protocol-http/src/api/auth.rs b/warpgate-protocol-http/src/api/auth.rs index 27d760dc8..db3b6d7ca 100644 --- a/warpgate-protocol-http/src/api/auth.rs +++ b/warpgate-protocol-http/src/api/auth.rs @@ -62,6 +62,30 @@ enum ApiAuthState { #[derive(Object)] struct LoginFailureResponse { state: ApiAuthState, + /// True when the credential the client just submitted was rejected + /// (as opposed to merely needing another factor). Lets the UI show an + /// "incorrect credentials" message and avoid auto-advancing to another + /// authentication method. + credential_rejected: bool, +} + +impl LoginFailureResponse { + /// A failure that is not caused by an invalid credential (e.g. blocked IP, + /// locked user, or simply a credential still being required). + fn state(state: ApiAuthState) -> Self { + Self { + state, + credential_rejected: false, + } + } + + /// A failure caused by the client submitting an invalid credential. + fn credential_rejected(state: ApiAuthState) -> Self { + Self { + state, + credential_rejected: true, + } + } } #[derive(ApiResponse)] @@ -164,9 +188,9 @@ impl Api { expires_at = %block_info.expires_at, "Login attempt from blocked IP" ); - return Ok(LoginResponse::Failure(Json(LoginFailureResponse { - state: ApiAuthState::IpBlocked, - }))); + return Ok(LoginResponse::Failure(Json(LoginFailureResponse::state( + ApiAuthState::IpBlocked, + )))); } } @@ -180,9 +204,9 @@ impl Api { username = %body.username, "Login attempt for locked user" ); - return Ok(LoginResponse::Failure(Json(LoginFailureResponse { - state: ApiAuthState::UserLocked, - }))); + return Ok(LoginResponse::Failure(Json(LoginFailureResponse::state( + ApiAuthState::UserLocked, + )))); } let state_arc = match get_or_create_auth_state_for_request(req, &body.username, &ctx).await @@ -207,9 +231,9 @@ impl Api { }) .await; } - return Ok(LoginResponse::Failure(Json(LoginFailureResponse { - state: ApiAuthState::Failed, - }))); + return Ok(LoginResponse::Failure(Json( + LoginFailureResponse::credential_rejected(ApiAuthState::Failed), + ))); } Err(WarpgateError::IpAddrNotAllowed(..)) => { let session_id = session_id_for_request(req, &ctx).await?; @@ -220,15 +244,15 @@ impl Api { "password", "IP address not allowed", ); - return Ok(LoginResponse::Failure(Json(LoginFailureResponse { - state: ApiAuthState::IpRejected, - }))); + return Ok(LoginResponse::Failure(Json(LoginFailureResponse::state( + ApiAuthState::IpRejected, + )))); } x => x, }?; let mut state = state_arc.lock().await; - validate_and_add_credential( + let credential_valid = validate_and_add_credential( &mut state, &AuthCredential::Password(Secret::new(body.password.clone())), &mut *ctx.services().config_provider.lock().await, @@ -258,21 +282,25 @@ impl Api { Ok(LoginResponse::Success) } x => { - error!("Auth rejected"); - // Record failed attempt on authentication failure - if let Some(ip) = client_ip { - let _ = services - .login_protection - .record_failed_attempt(FailedAttemptInfo { - username: state.user_info().username.clone(), - remote_ip: ip, - protocol: "http".to_string(), - credential_type: "password".to_string(), - }) - .await; + // Only an invalid password counts as a failed attempt; a valid + // password that merely needs a second factor is not a failure. + if !credential_valid { + error!("Password authentication failed"); + if let Some(ip) = client_ip { + let _ = services + .login_protection + .record_failed_attempt(FailedAttemptInfo { + username: state.user_info().username.clone(), + remote_ip: ip, + protocol: "http".to_string(), + credential_type: "password".to_string(), + }) + .await; + } } Ok(LoginResponse::Failure(Json(LoginFailureResponse { state: x.into(), + credential_rejected: !credential_valid, }))) } } @@ -298,16 +326,16 @@ impl Api { expires_at = %block_info.expires_at, "OTP login attempt from blocked IP" ); - return Ok(LoginResponse::Failure(Json(LoginFailureResponse { - state: ApiAuthState::IpBlocked, - }))); + return Ok(LoginResponse::Failure(Json(LoginFailureResponse::state( + ApiAuthState::IpBlocked, + )))); } } let Some(state_arc) = get_auth_state_for_request(req, &ctx).await? else { - return Ok(LoginResponse::Failure(Json(LoginFailureResponse { - state: ApiAuthState::NotStarted, - }))); + return Ok(LoginResponse::Failure(Json(LoginFailureResponse::state( + ApiAuthState::NotStarted, + )))); }; let mut state = state_arc.lock().await; @@ -322,12 +350,12 @@ impl Api { username = %state.user_info().username, "OTP login attempt for locked user" ); - return Ok(LoginResponse::Failure(Json(LoginFailureResponse { - state: ApiAuthState::UserLocked, - }))); + return Ok(LoginResponse::Failure(Json(LoginFailureResponse::state( + ApiAuthState::UserLocked, + )))); } - validate_and_add_credential( + let credential_valid = validate_and_add_credential( &mut state, &AuthCredential::Otp(body.otp.clone().into()), &mut *services.config_provider.lock().await, @@ -357,20 +385,23 @@ impl Api { Ok(LoginResponse::Success) } x => { - // Record failed attempt on authentication failure - if let Some(ip) = client_ip { - let _ = services - .login_protection - .record_failed_attempt(FailedAttemptInfo { - username: state.user_info().username.clone(), - remote_ip: ip, - protocol: "http".to_string(), - credential_type: "otp".to_string(), - }) - .await; + // Only an invalid OTP counts as a failed attempt. + if !credential_valid { + if let Some(ip) = client_ip { + let _ = services + .login_protection + .record_failed_attempt(FailedAttemptInfo { + username: state.user_info().username.clone(), + remote_ip: ip, + protocol: "http".to_string(), + credential_type: "otp".to_string(), + }) + .await; + } } Ok(LoginResponse::Failure(Json(LoginFailureResponse { state: x.into(), + credential_rejected: !credential_valid, }))) } } diff --git a/warpgate-web/src/admin/lib/openapi-schema.json b/warpgate-web/src/admin/lib/openapi-schema.json index 65eb8db30..6c10205e4 100644 --- a/warpgate-web/src/admin/lib/openapi-schema.json +++ b/warpgate-web/src/admin/lib/openapi-schema.json @@ -2,7 +2,7 @@ "openapi": "3.0.0", "info": { "title": "Warpgate Web Admin", - "version": "v0.25.5-40-g6c82ea58-modified" + "version": "v0.25.5-19-g63bf4ede-modified" }, "servers": [ { diff --git a/warpgate-web/src/gateway/Login.svelte b/warpgate-web/src/gateway/Login.svelte index 92f3ce973..df0b686b9 100644 --- a/warpgate-web/src/gateway/Login.svelte +++ b/warpgate-web/src/gateway/Login.svelte @@ -17,6 +17,7 @@ let password = $state('') let otp = $state('') let busy = $state(false) + let credentialRejected = $state(false) let otpInput: HTMLInputElement|undefined = $state() let authState: ApiAuthState|undefined = $state() let ssoProvidersPromise = api.getSsoProviders() @@ -49,15 +50,12 @@ } } - async function continueWithState () { + async function continueWithState ({ allowSsoRedirect = true } = {}) { if (authState === ApiAuthState.Success) { success() } - if (authState === ApiAuthState.SsoNeeded) { + if (authState === ApiAuthState.SsoNeeded && allowSsoRedirect) { const providers = await ssoProvidersPromise - if (!providers.length) { - // todo - } if (providers.length === 1) { startSSO(providers[0]!) } @@ -80,6 +78,7 @@ async function _login () { error = null + credentialRejected = false try { if (authState === ApiAuthState.OtpNeeded) { await api.otpLogin({ @@ -102,8 +101,12 @@ if (err.response.status === 401) { const failure = LoginFailureResponseFromJSON(await err.response.json()) authState = failure.state + credentialRejected = failure.credentialRejected ?? false - continueWithState() + // Don't auto-advance to another auth method (e.g. SSO) when + // the submitted credential was rejected — show the error and + // let the user retry or pick a method themselves. + continueWithState({ allowSsoRedirect: !credentialRejected }) } else { error = await err.response.text() } @@ -216,7 +219,7 @@
- {#if authState === ApiAuthState.Failed} + {#if credentialRejected || authState === ApiAuthState.Failed} Incorrect credentials {/if} {#if authState === ApiAuthState.IpRejected} diff --git a/warpgate-web/src/gateway/lib/openapi-schema.json b/warpgate-web/src/gateway/lib/openapi-schema.json index fc13e2647..b5a71552c 100644 --- a/warpgate-web/src/gateway/lib/openapi-schema.json +++ b/warpgate-web/src/gateway/lib/openapi-schema.json @@ -2,7 +2,7 @@ "openapi": "3.0.0", "info": { "title": "Warpgate HTTP proxy", - "version": "v0.25.5-37-g69c02d3f-modified" + "version": "v0.25.5-19-g63bf4ede-modified" }, "servers": [ { @@ -1844,11 +1844,16 @@ "type": "object", "title": "LoginFailureResponse", "required": [ - "state" + "state", + "credential_rejected" ], "properties": { "state": { "$ref": "#/components/schemas/ApiAuthState" + }, + "credential_rejected": { + "type": "boolean", + "description": "True when the credential the client just submitted was rejected\n(as opposed to merely needing another factor). Lets the UI show an\n\"incorrect credentials\" message and avoid auto-advancing to another\nauthentication method." } } }, From e9f76f750e5980ecc35086daecc1dbbea90946cc Mon Sep 17 00:00:00 2001 From: Eugene Date: Mon, 29 Jun 2026 20:39:48 +0200 Subject: [PATCH 232/556] bump anyhow --- Cargo.lock | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index b50467baa..1927bd298 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -162,9 +162,9 @@ dependencies = [ [[package]] name = "anyhow" -version = "1.0.102" +version = "1.0.103" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7f202df86484c868dbad7eaa557ef785d5c66295e41b460ef922eca0723b842c" +checksum = "2a4385e2e34eb35d6b3efe798b9eb88096925d87726c0798709bf56d9ed84af3" [[package]] name = "approx" From 131c8c32715421f84622eb2bd99e5e517eccf24f Mon Sep 17 00:00:00 2001 From: Eugene Date: Mon, 29 Jun 2026 22:56:52 +0200 Subject: [PATCH 233/556] added migrate-database cli command (#2123) --- justfile | 4 ++-- warpgate-core/src/db/mod.rs | 23 ++++++++++++++++++++++- warpgate-core/src/services.rs | 4 ++-- warpgate-db-migrations/src/lib.rs | 16 ++++++++++++++++ warpgate/src/commands/migrate.rs | 23 +++++++++++++++++++++++ warpgate/src/commands/mod.rs | 1 + warpgate/src/commands/setup.rs | 4 ++-- warpgate/src/main.rs | 18 ++++++++++++++++++ 8 files changed, 86 insertions(+), 7 deletions(-) create mode 100644 warpgate/src/commands/migrate.rs diff --git a/justfile b/justfile index c6e601279..1ac053aa8 100644 --- a/justfile +++ b/justfile @@ -1,7 +1,7 @@ projects := "warpgate warpgate-admin warpgate-common warpgate-db-entities warpgate-db-migrations warpgate-database-protocols warpgate-protocol-ssh warpgate-protocol-mysql warpgate-protocol-postgres warpgate-protocol-kubernetes warpgate-protocol-http warpgate-core warpgate-sso" -run $RUST_BACKTRACE='1' *ARGS='run': - cargo run --all-features -- --config config.yaml {{ARGS}} +run *ARGS='run': + RUST_BACKTRACE=1 cargo run --all-features -- --config config.yaml {{ARGS}} fmt: for p in {{projects}}; do cargo fmt -p $p -v; done diff --git a/warpgate-core/src/db/mod.rs b/warpgate-core/src/db/mod.rs index 75cefac72..c17fabb66 100644 --- a/warpgate-core/src/db/mod.rs +++ b/warpgate-core/src/db/mod.rs @@ -10,10 +10,11 @@ use time::OffsetDateTime; use tracing::error; use warpgate_common::helpers::fs::secure_file; use warpgate_common::{GlobalParams, WarpgateConfig, WarpgateError}; -use warpgate_db_migrations::migrate_database; +use warpgate_db_migrations::{migrate_database, migrate_database_down, migrate_database_up}; use crate::recordings::SessionRecordings; +/// Open a connection to the configured database without running migrations. pub async fn connect_to_db( config: &WarpgateConfig, params: &GlobalParams, @@ -55,10 +56,30 @@ pub async fn connect_to_db( let connection = Database::connect(opt).await?; + Ok(connection) +} + +pub async fn connect_to_db_and_migrate( + config: &WarpgateConfig, + params: &GlobalParams, +) -> Result { + let connection = connect_to_db(config, params).await?; migrate_database(&connection).await?; Ok(connection) } +/// Apply `steps` pending migrations. +pub async fn migrate_up(connection: &DatabaseConnection, steps: u32) -> Result<()> { + migrate_database_up(connection, steps).await?; + Ok(()) +} + +/// Revert `steps` applied migrations. +pub async fn migrate_down(connection: &DatabaseConnection, steps: u32) -> Result<()> { + migrate_database_down(connection, steps).await?; + Ok(()) +} + pub async fn populate_db( db: &DatabaseConnection, _config: &mut WarpgateConfig, diff --git a/warpgate-core/src/services.rs b/warpgate-core/src/services.rs index fa888b7f9..f43739656 100644 --- a/warpgate-core/src/services.rs +++ b/warpgate-core/src/services.rs @@ -7,7 +7,7 @@ use tokio::sync::Mutex; use tracing::warn; use warpgate_common::{GlobalParams, WarpgateConfig}; -use crate::db::{connect_to_db, populate_db}; +use crate::db::{connect_to_db_and_migrate, populate_db}; use crate::login_protection::LoginProtectionService; use crate::rate_limiting::RateLimiterRegistry; use crate::recordings::SessionRecordings; @@ -33,7 +33,7 @@ impl Services { admin_token: Option, params: GlobalParams, ) -> Result { - let db = connect_to_db(&config, ¶ms).await?; + let db = connect_to_db_and_migrate(&config, ¶ms).await?; populate_db(&db, &mut config).await?; let db = Arc::new(Mutex::new(db)); diff --git a/warpgate-db-migrations/src/lib.rs b/warpgate-db-migrations/src/lib.rs index b5ec6c872..b976362e5 100644 --- a/warpgate-db-migrations/src/lib.rs +++ b/warpgate-db-migrations/src/lib.rs @@ -126,3 +126,19 @@ impl MigratorTrait for Migrator { pub async fn migrate_database(connection: &DatabaseConnection) -> Result<(), DbErr> { Migrator::up(connection, None).await } + +/// Apply `steps` pending migrations. +pub async fn migrate_database_up( + connection: &DatabaseConnection, + steps: u32, +) -> Result<(), DbErr> { + Migrator::up(connection, Some(steps)).await +} + +/// Revert `steps` applied migrations. +pub async fn migrate_database_down( + connection: &DatabaseConnection, + steps: u32, +) -> Result<(), DbErr> { + Migrator::down(connection, Some(steps)).await +} diff --git a/warpgate/src/commands/migrate.rs b/warpgate/src/commands/migrate.rs new file mode 100644 index 000000000..d7946a0aa --- /dev/null +++ b/warpgate/src/commands/migrate.rs @@ -0,0 +1,23 @@ +use anyhow::Result; +use tracing::info; +use warpgate_common::GlobalParams; +use warpgate_core::db::{connect_to_db, migrate_down, migrate_up}; + +use crate::config::load_config; + +pub async fn command(params: &GlobalParams, steps: i32) -> Result<()> { + let config = load_config(params, true)?; + let connection = connect_to_db(&config, params).await?; + + if steps < 0 { + let steps = steps.unsigned_abs(); + info!("Reverting {steps} migration(s)"); + migrate_down(&connection, steps).await?; + } else { + let steps = steps.unsigned_abs(); + info!("Applying {steps} migration(s)"); + migrate_up(&connection, steps).await?; + } + + Ok(()) +} diff --git a/warpgate/src/commands/mod.rs b/warpgate/src/commands/mod.rs index 43ab3d795..3013d2bcc 100644 --- a/warpgate/src/commands/mod.rs +++ b/warpgate/src/commands/mod.rs @@ -3,6 +3,7 @@ pub mod client_keys; mod common; pub mod create_user; pub mod healthcheck; +pub mod migrate; pub mod recover_access; pub mod run; pub mod setup; diff --git a/warpgate/src/commands/setup.rs b/warpgate/src/commands/setup.rs index 3489f3663..023fc68cd 100644 --- a/warpgate/src/commands/setup.rs +++ b/warpgate/src/commands/setup.rs @@ -19,7 +19,7 @@ use warpgate_common::{ Secret, SshConfig, WarpgateConfigStore, }; use warpgate_core::consts::{BUILTIN_ADMIN_ROLE_NAME, BUILTIN_ADMIN_USERNAME}; -use warpgate_core::db::connect_to_db; +use warpgate_core::db::connect_to_db_and_migrate; use warpgate_db_entities::{Role, User, UserRoleAssignment}; use crate::commands::common::{assert_interactive_terminal, is_docker}; @@ -345,7 +345,7 @@ pub async fn command(cli: &Cli, params: &GlobalParams) -> Result<()> { ) .await?; - let db = connect_to_db(&config, params).await?; + let db = connect_to_db_and_migrate(&config, params).await?; #[allow(clippy::expect_used)] let user = User::Entity::find() diff --git a/warpgate/src/main.rs b/warpgate/src/main.rs index ec51e99d7..d2d193cb2 100644 --- a/warpgate/src/main.rs +++ b/warpgate/src/main.rs @@ -116,6 +116,15 @@ pub(crate) enum Commands { #[clap(action=ArgAction::Set)] username: Option, }, + /// Run database migrations + #[clap(allow_negative_numbers = true)] + MigrateDatabase { + /// Number of migrations to apply (positive) or revert (negative) + #[clap(value_name = "STEPS", default_value = "1", allow_hyphen_values = true)] + steps: i32, + #[clap(long, action=ArgAction::SetTrue)] + destructive: bool, + }, /// Show version information Version, /// Automatic healthcheck for running Warpgate in a container @@ -176,6 +185,15 @@ async fn _main() -> Result<()> { crate::commands::recover_access::command(¶ms, username.as_ref()).await } Commands::Healthcheck => crate::commands::healthcheck::command(¶ms).await, + Commands::MigrateDatabase { steps, destructive } => { + if *steps < 0 && !destructive { + error!( + "Reverting migrations is a destructive operation. Use the --destructive flag to confirm." + ); + std::process::exit(1); + } + crate::commands::migrate::command(¶ms, *steps).await + } } } From ff90ff828605629dd9d1dac7df0c5636ba63bce4 Mon Sep 17 00:00:00 2001 From: Eugene Date: Mon, 29 Jun 2026 23:59:38 +0200 Subject: [PATCH 234/556] fixed #2112 - support receiving pre-expired cookies from upstream (#2126) --- warpgate-protocol-http/src/proxy.rs | 46 ++++++++++++++++++++++++++++- 1 file changed, 45 insertions(+), 1 deletion(-) diff --git a/warpgate-protocol-http/src/proxy.rs b/warpgate-protocol-http/src/proxy.rs index 65e5baab6..5a1f1dcbe 100644 --- a/warpgate-protocol-http/src/proxy.rs +++ b/warpgate-protocol-http/src/proxy.rs @@ -186,7 +186,14 @@ fn rewrite_response( for value in entry.iter_mut() { try_block!({ let mut cookie = Cookie::parse(value.to_str()?)?; - cookie.set_expires(cookie::Expiration::Session); + // Some apps clear a cookie by re-setting it with an expiration + // in the past. We keep these as-is + // https://github.com/warp-tech/warpgate/issues/2112 + if let Some(cookie::Expiration::DateTime(expires)) = cookie.expires() + && expires >= cookie::time::OffsetDateTime::now_utc() + { + cookie.set_expires(cookie::Expiration::Session); + } // the domain set by the target isn't going to match the actual host anyway // https://github.com/warp-tech/warpgate/issues/2048 cookie.unset_domain(); @@ -628,4 +635,41 @@ mod tests { assert_eq!(cookie.http_only(), Some(true)); assert_eq!(cookie.secure(), Some(true)); } + + fn rewrite_cookie(set_cookie: &str) -> Cookie<'static> { + let mut resp = poem::Response::builder() + .header(http::header::SET_COOKIE, set_cookie) + .body(()); + let options = make_options("https://100.0.0.1:7080"); + let source_uri = Uri::try_from("https://100.0.0.1:7080/index.php").unwrap(); + rewrite_response(&mut resp, &options, &source_uri).unwrap(); + let cookie_headers: Vec<_> = resp + .headers() + .get_all(http::header::SET_COOKIE) + .iter() + .map(|v| v.to_str().unwrap().to_string()) + .collect(); + assert_eq!(cookie_headers.len(), 1); + Cookie::parse(cookie_headers[0].clone()).unwrap() + } + + #[test] + fn rewrite_response_keeps_past_expiration() { + // A past-dated deletion cookie would otherwise drop the live session + // cookie and cause a login loop. https://github.com/warp-tech/warpgate/issues/2112 + let cookie = + rewrite_cookie("lsws_uid=deleted; Path=/; Expires=Thu, 01 Jan 1970 00:00:00 GMT"); + assert_eq!(cookie.value(), "deleted"); + assert!(matches!( + cookie.expires(), + Some(cookie::Expiration::DateTime(_)) + )); + } + + #[test] + fn rewrite_response_removes_future_expiration() { + // A genuine persistent cookie must keep the expiry the origin set. + let cookie = rewrite_cookie("lsws_uid=abc; Path=/; Expires=Tue, 01 Jan 2999 00:00:00 GMT"); + assert_eq!(cookie.expires(), None); + } } From 2eaf77757cae84a4873e88b0214d9d1fe5e19e8d Mon Sep 17 00:00:00 2001 From: Eugene Date: Tue, 30 Jun 2026 00:00:20 +0200 Subject: [PATCH 235/556] fixed #2108 - global toggle for webSSH (#2119) --- warpgate-admin/src/api/parameters.rs | 4 + warpgate-db-entities/src/Parameters.rs | 2 + warpgate-db-migrations/src/lib.rs | 2 + .../src/m00056_web_ssh_enabled.rs | 36 ++ warpgate-protocol-http/src/api/info.rs | 2 + warpgate-protocol-http/src/api/web_ssh.rs | 9 + .../src/admin/config/LoginProtection.svelte | 5 - .../src/admin/config/Parameters.svelte | 334 +++++++++--------- warpgate-web/src/admin/lib/HelpText.svelte | 27 ++ warpgate-web/src/admin/lib/Section.svelte | 2 +- warpgate-web/src/admin/lib/Subsection.svelte | 34 ++ .../src/admin/lib/openapi-schema.json | 11 +- warpgate-web/src/common/ThemeSwitcher.svelte | 2 +- warpgate-web/src/gateway/TargetList.svelte | 17 +- .../src/gateway/lib/openapi-schema.json | 6 +- 15 files changed, 313 insertions(+), 180 deletions(-) create mode 100644 warpgate-db-migrations/src/m00056_web_ssh_enabled.rs create mode 100644 warpgate-web/src/admin/lib/HelpText.svelte create mode 100644 warpgate-web/src/admin/lib/Subsection.svelte diff --git a/warpgate-admin/src/api/parameters.rs b/warpgate-admin/src/api/parameters.rs index c6f16f7fc..1bd400280 100644 --- a/warpgate-admin/src/api/parameters.rs +++ b/warpgate-admin/src/api/parameters.rs @@ -45,6 +45,7 @@ struct ParameterValues { pub lp_user_auto_unlock: bool, pub lp_user_lockout_duration_seconds: i32, pub lp_user_exempt_admins: bool, + pub web_ssh_enabled: bool, } #[derive(Serialize, Object)] @@ -79,6 +80,7 @@ struct ParameterUpdate { pub lp_user_auto_unlock: Option, pub lp_user_lockout_duration_seconds: Option, pub lp_user_exempt_admins: Option, + pub web_ssh_enabled: Option, } #[derive(ApiResponse)] @@ -137,6 +139,7 @@ impl Api { lp_user_auto_unlock: parameters.lp_user_auto_unlock, lp_user_lockout_duration_seconds: parameters.lp_user_lockout_duration_seconds, lp_user_exempt_admins: parameters.lp_user_exempt_admins, + web_ssh_enabled: parameters.web_ssh_enabled, }))) } @@ -212,6 +215,7 @@ impl Api { parameters.lp_user_lockout_duration_seconds = body.lp_user_lockout_duration_seconds.map_or(NotSet, Set); parameters.lp_user_exempt_admins = body.lp_user_exempt_admins.map_or(NotSet, Set); + parameters.web_ssh_enabled = body.web_ssh_enabled.map_or(NotSet, Set); Parameters::Entity::update(parameters).exec(&*db).await?; drop(db); diff --git a/warpgate-db-entities/src/Parameters.rs b/warpgate-db-entities/src/Parameters.rs index 194c7bc6d..f4083d85a 100644 --- a/warpgate-db-entities/src/Parameters.rs +++ b/warpgate-db-entities/src/Parameters.rs @@ -58,6 +58,7 @@ pub struct Model { pub lp_user_auto_unlock: bool, pub lp_user_lockout_duration_seconds: i32, pub lp_user_exempt_admins: bool, + pub web_ssh_enabled: bool, } impl ActiveModelBehavior for ActiveModel {} @@ -121,6 +122,7 @@ impl Entity { lp_user_auto_unlock: Set(true), lp_user_lockout_duration_seconds: Set(3600), lp_user_exempt_admins: Set(true), + web_ssh_enabled: Set(true), } .insert(db) .await diff --git a/warpgate-db-migrations/src/lib.rs b/warpgate-db-migrations/src/lib.rs index b976362e5..34b3b35ad 100644 --- a/warpgate-db-migrations/src/lib.rs +++ b/warpgate-db-migrations/src/lib.rs @@ -56,6 +56,7 @@ mod m00051_tutorial_dismissed; mod m00052_log_text_column; mod m00053_login_protection; mod m00054_login_protection_params; +mod m00056_web_ssh_enabled; pub(crate) mod helpers; @@ -119,6 +120,7 @@ impl MigratorTrait for Migrator { Box::new(m00052_log_text_column::Migration), Box::new(m00053_login_protection::Migration), Box::new(m00054_login_protection_params::Migration), + Box::new(m00056_web_ssh_enabled::Migration), ] } } diff --git a/warpgate-db-migrations/src/m00056_web_ssh_enabled.rs b/warpgate-db-migrations/src/m00056_web_ssh_enabled.rs new file mode 100644 index 000000000..2953b3cb1 --- /dev/null +++ b/warpgate-db-migrations/src/m00056_web_ssh_enabled.rs @@ -0,0 +1,36 @@ +use sea_orm_migration::prelude::*; + +use crate::m00010_parameters::parameters; + +#[derive(DeriveMigrationName)] +pub struct Migration; + +#[async_trait::async_trait] +impl MigrationTrait for Migration { + async fn up(&self, manager: &SchemaManager) -> Result<(), DbErr> { + manager + .alter_table( + Table::alter() + .table(parameters::Entity) + .add_column( + ColumnDef::new(Alias::new("web_ssh_enabled")) + .boolean() + .not_null() + .default(true), + ) + .to_owned(), + ) + .await + } + + async fn down(&self, manager: &SchemaManager) -> Result<(), DbErr> { + manager + .alter_table( + Table::alter() + .table(parameters::Entity) + .drop_column(Alias::new("web_ssh_enabled")) + .to_owned(), + ) + .await + } +} diff --git a/warpgate-protocol-http/src/api/info.rs b/warpgate-protocol-http/src/api/info.rs index e2f94be33..79f23e25b 100644 --- a/warpgate-protocol-http/src/api/info.rs +++ b/warpgate-protocol-http/src/api/info.rs @@ -95,6 +95,7 @@ pub struct Info { ticket_request_show_all_targets: bool, target_click_action: Parameters::TargetClickAction, max_api_token_duration_seconds: Option, + web_ssh_enabled: bool, has_ldap: bool, setup_state: Option, admin_permissions: Option, @@ -313,6 +314,7 @@ impl Api { ticket_request_show_all_targets: parameters.ticket_request_show_all_targets, target_click_action: parameters.target_click_action, max_api_token_duration_seconds: parameters.max_api_token_duration_seconds, + web_ssh_enabled: parameters.web_ssh_enabled, setup_state, has_ldap: auth_ctx.is_some() && has_ldap, admin_permissions, diff --git a/warpgate-protocol-http/src/api/web_ssh.rs b/warpgate-protocol-http/src/api/web_ssh.rs index 8bafd02d9..95900719c 100644 --- a/warpgate-protocol-http/src/api/web_ssh.rs +++ b/warpgate-protocol-http/src/api/web_ssh.rs @@ -10,6 +10,7 @@ use uuid::Uuid; use warpgate_common::WarpgateError; use warpgate_common_http::auth::AuthenticatedRequestContext; use warpgate_core::ConfigProvider; +use warpgate_db_entities::Parameters; use warpgate_db_entities::Target::{self, TargetKind}; use warpgate_web_ssh::WebSshClientManager; @@ -84,6 +85,14 @@ impl Api { return Ok(CreateWebSshSessionResponse::Forbidden); }; + if !Parameters::Entity::get(&*ctx.services().db.lock().await) + .await + .map_err(WarpgateError::from)? + .web_ssh_enabled + { + return Ok(CreateWebSshSessionResponse::Forbidden); + } + let Some(target) = Target::Entity::find_by_id(body.target_id) .one(&*ctx.services().db.lock().await) .await diff --git a/warpgate-web/src/admin/config/LoginProtection.svelte b/warpgate-web/src/admin/config/LoginProtection.svelte index 0a01e77a0..8248087e2 100644 --- a/warpgate-web/src/admin/config/LoginProtection.svelte +++ b/warpgate-web/src/admin/config/LoginProtection.svelte @@ -170,9 +170,4 @@ gap: .5rem; margin-bottom: .5rem; } - - .section-title { - font-weight: 600; - font-size: .95rem; - } diff --git a/warpgate-web/src/admin/config/Parameters.svelte b/warpgate-web/src/admin/config/Parameters.svelte index ae18feb89..2a1af0d14 100644 --- a/warpgate-web/src/admin/config/Parameters.svelte +++ b/warpgate-web/src/admin/config/Parameters.svelte @@ -15,6 +15,8 @@ import SectionedForm from 'admin/lib/SectionedForm.svelte' import Section from 'admin/lib/Section.svelte' import StickyActionBar from 'common/StickyActionBar.svelte' + import Subsection from 'admin/lib/Subsection.svelte'; + import HelpText from 'admin/lib/HelpText.svelte'; let parameters: ParameterValues | undefined = $state() let hasSsoProviders = $state(false) @@ -152,73 +154,86 @@
- - + - +
- - - - - - - Controls which authentication methods are offered to SSH clients. - Disabling password authentication can help prevent brute-force attacks. - + + + -
+ + + + + + Controls which authentication methods are offered to SSH clients. + Disabling password authentication can help prevent brute-force attacks. + + - - - Legacy SCP works over an exec channel and would be normally recorded like any other command. Disable to prevent SCP recordings from wasting storage space. - + + + + Legacy SCP works over an exec channel and would be normally recorded like any other command. Disable to prevent SCP recordings from wasting storage space. + +
@@ -273,34 +288,36 @@ bind:checked={parameters.ticketRequestShowAllTargets} />
Show all targets in ticket request form
- + When disabled, users only see targets they already have role-based access to. - - - - { parameters!.ticketMaxDurationSeconds = v } }} - /> - + + + + + { parameters!.ticketMaxDurationSeconds = v } }} + /> + + Global default. Can be overridden per target. Examples: 30m, 8h, 1d, 2h30m. - - - - - { - const v = parseInt(e.currentTarget.value) - parameters!.ticketMaxUses = isNaN(v) ? undefined : v - }} - /> - + + + + { + const v = parseInt(e.currentTarget.value) + parameters!.ticketMaxUses = isNaN(v) ? undefined : v + }} + /> + + {/if}
@@ -339,9 +356,9 @@ bind:checked={parameters.showSessionMenu} />
Show HTTP session menu
- + Warpgate can inject a session menu into HTTP sessions, allowing users to log out or return back to the home page. - + {#if hasSsoProviders} @@ -357,9 +374,9 @@ bind:checked={parameters.minimizePasswordLogin} />
Minimize password login UI
- + When enabled, the username and password fields are hidden behind a link on the login page, with the focus on the SSO buttons. - + {/if} @@ -376,16 +393,16 @@ bind:checked={parameters.loginProtectionEnabled} />
Enable brute-force protection
- + Rate-limits IPs and locks accounts after repeated failed logins. When disabled, all settings below are preserved but not enforced. - + {#if lpCapWarning} {lpCapWarning} {/if} {#if parameters.loginProtectionEnabled} -

IP rate-limit

+
@@ -428,70 +445,76 @@
- + Each block is multiplier × the previous block duration, capped at the maximum. The repeat count resets only after the cooldown period of clean activity — not when a block expires. - - -

User lockout

-
-
- - { parameters!.lpUserMaxAttempts = e.currentTarget.valueAsNumber }} /> - + + + +
+
+ + { parameters!.lpUserMaxAttempts = e.currentTarget.valueAsNumber }} /> + +
+
+ + { if (v != null) { parameters!.lpUserTimeWindowSeconds = v } } }} /> + +
-
- + + {#if parameters.lpUserAutoUnlock} + { if (v != null) { parameters!.lpUserTimeWindowSeconds = v } } }} /> + use:humantimeDuration={{ seconds: parameters.lpUserLockoutDurationSeconds, onChange: v => { if (v != null) { parameters!.lpUserLockoutDurationSeconds = v } } }} /> -
-
- - {#if parameters.lpUserAutoUnlock} - - { if (v != null) { parameters!.lpUserLockoutDurationSeconds = v } } }} /> + {/if} + + + + + + Recommended: keeps an attacker from locking out an admin account by spamming its username. IP blocking still applies to everyone. + + + + + + { if (v != null) { parameters!.loginProtectionRetentionSeconds = v } } }} /> - {/if} - - - Recommended: keeps an attacker from locking out an admin account by spamming its username. IP blocking still applies to everyone. - + -

Data retention

- - { if (v != null) { parameters!.loginProtectionRetentionSeconds = v } } }} /> - - - + Manage active blocks & lockouts on the Login protection page. - + + {/if} @@ -506,18 +529,3 @@
- - diff --git a/warpgate-web/src/admin/lib/HelpText.svelte b/warpgate-web/src/admin/lib/HelpText.svelte new file mode 100644 index 000000000..2573ffdd2 --- /dev/null +++ b/warpgate-web/src/admin/lib/HelpText.svelte @@ -0,0 +1,27 @@ + + + + {@render props.children?.()} + + + diff --git a/warpgate-web/src/admin/lib/Section.svelte b/warpgate-web/src/admin/lib/Section.svelte index 0e6cb7fe8..31b1caae4 100644 --- a/warpgate-web/src/admin/lib/Section.svelte +++ b/warpgate-web/src/admin/lib/Section.svelte @@ -26,7 +26,7 @@ } .sectioned-form-section { - padding: 0 0 1.5rem; + margin: 0 0 3rem; } .sectioned-form-section:last-child { diff --git a/warpgate-web/src/admin/lib/Subsection.svelte b/warpgate-web/src/admin/lib/Subsection.svelte new file mode 100644 index 000000000..fb7e684d3 --- /dev/null +++ b/warpgate-web/src/admin/lib/Subsection.svelte @@ -0,0 +1,34 @@ + + +
+
{props.title}
+

+ {@render props.children?.()} +

+
+ + diff --git a/warpgate-web/src/admin/lib/openapi-schema.json b/warpgate-web/src/admin/lib/openapi-schema.json index 6c10205e4..0eed6601b 100644 --- a/warpgate-web/src/admin/lib/openapi-schema.json +++ b/warpgate-web/src/admin/lib/openapi-schema.json @@ -2,7 +2,7 @@ "openapi": "3.0.0", "info": { "title": "Warpgate Web Admin", - "version": "v0.25.5-19-g63bf4ede-modified" + "version": "v0.25.5-25-gaf4df05b-modified" }, "servers": [ { @@ -5303,6 +5303,9 @@ }, "lp_user_exempt_admins": { "type": "boolean" + }, + "web_ssh_enabled": { + "type": "boolean" } } }, @@ -5335,7 +5338,8 @@ "lp_user_time_window_seconds", "lp_user_auto_unlock", "lp_user_lockout_duration_seconds", - "lp_user_exempt_admins" + "lp_user_exempt_admins", + "web_ssh_enabled" ], "properties": { "allow_own_credential_management": { @@ -5441,6 +5445,9 @@ }, "lp_user_exempt_admins": { "type": "boolean" + }, + "web_ssh_enabled": { + "type": "boolean" } } }, diff --git a/warpgate-web/src/common/ThemeSwitcher.svelte b/warpgate-web/src/common/ThemeSwitcher.svelte index d382032cf..c07ccfdae 100644 --- a/warpgate-web/src/common/ThemeSwitcher.svelte +++ b/warpgate-web/src/common/ThemeSwitcher.svelte @@ -27,7 +27,7 @@ {/if} - + {#if $currentTheme === 'dark'} Dark theme {:else if $currentTheme === 'light'} diff --git a/warpgate-web/src/gateway/TargetList.svelte b/warpgate-web/src/gateway/TargetList.svelte index aef909e69..af7131bae 100644 --- a/warpgate-web/src/gateway/TargetList.svelte +++ b/warpgate-web/src/gateway/TargetList.svelte @@ -16,6 +16,7 @@ import GroupColorCircle from 'common/GroupColorCircle.svelte' let instructionsTarget: TargetSnapshot|undefined = $state() const canEditTargets = $derived($serverInfo?.adminPermissions?.targetsEdit ?? false) +const webSshEnabled = $derived($serverInfo?.webSshEnabled ?? true) async function openWebSsh (target: TargetSnapshot) { const { sessionId } = await api.createWebSshSession({ @@ -68,7 +69,7 @@ function selectTarget (target: TargetSnapshot) { } } else if (target.kind === TargetKind.Ssh) { const targetClickAction = $serverInfo?.targetClickAction - if (targetClickAction === TargetClickAction.ShowInstructions) { + if (!webSshEnabled || targetClickAction === TargetClickAction.ShowInstructions) { instructionsTarget = target } else { openWebSsh(target) @@ -181,11 +182,13 @@ function groupInfoFromTarget (target: TargetSnapshot): GroupInfo { {#if target.kind === TargetKind.Ssh} - { - openWebSsh(target) - e.preventDefault() - e.stopPropagation() - }}>Web terminal + {#if webSshEnabled} + { + openWebSsh(target) + e.preventDefault() + e.stopPropagation() + }}>Web terminal + {/if} { showInstructions(target) e.preventDefault() @@ -229,7 +232,7 @@ function groupInfoFromTarget (target: TargetSnapshot): GroupInfo { {/if} - {#if instructionsTarget?.kind === TargetKind.Ssh} + {#if instructionsTarget?.kind === TargetKind.Ssh && webSshEnabled}
+{#if showAnalyticsModal} + +{/if} + diff --git a/warpgate-web/src/admin/player/PlayerToolbar.svelte b/warpgate-web/src/admin/player/PlayerToolbar.svelte new file mode 100644 index 000000000..301ecc106 --- /dev/null +++ b/warpgate-web/src/admin/player/PlayerToolbar.svelte @@ -0,0 +1,170 @@ + + +
+ +
{ formatDuration(timestamp * 1000, { leading: true }) }
+ {#if isLive} + {#if liveActive} +
+ + Live +
+ {:else} + + + Go live + + {/if} + {/if} +
+ {#if heatmap} + + {/if} + onSeek(seekInputValue)} /> +
+ +
+ + diff --git a/warpgate-web/src/admin/player/TerminalRecordingPlayer.svelte b/warpgate-web/src/admin/player/TerminalRecordingPlayer.svelte index 8d0b653fd..6b5ea671f 100644 --- a/warpgate-web/src/admin/player/TerminalRecordingPlayer.svelte +++ b/warpgate-web/src/admin/player/TerminalRecordingPlayer.svelte @@ -3,10 +3,11 @@ import { onDestroy, onMount } from 'svelte' import { Terminal } from '@xterm/xterm' import { SerializeAddon } from '@xterm/addon-serialize' - import { faPlay, faPause, faExpand } from '@fortawesome/free-solid-svg-icons' + import { faPlay } from '@fortawesome/free-solid-svg-icons' import { Spinner } from '@sveltestrap/sveltestrap' - import formatDuration from 'format-duration' import type { Recording } from 'admin/lib/api' + import PlayerToolbar from './PlayerToolbar.svelte' + import { latestWins } from './latestWins' export let recording: Recording @@ -112,7 +113,9 @@ addData(JSON.parse(line)) } - await seek(duration) + // Await the first paint directly (nothing else is seeking yet) so `loading` clears + // only once the terminal reflects the recording. + await _seekInternal(duration) socket = new WebSocket(`wss://${location.host}/@warpgate/admin/api/recordings/${recording.id}/stream`) socket.addEventListener('message', function (event) { @@ -186,11 +189,13 @@ term.options.fontSize = fontWidth / (metrics.width / 6) * 10 } - let seekPromise = Promise.resolve() + // Shared latest-wins runner: serializes seeks and coalesces rapid scrubs to the newest + // target (replaying the terminal to an intermediate position we're about to leave is + // wasted work). Reconstructing state at `time` is independent of skipped seeks. + const runSeek = latestWins((time: number) => _seekInternal(time)) - async function seek (time: number) { - seekPromise = seekPromise.then(() => _seekInternal(time)) - await seekPromise + function seek (time: number) { + runSeek(time) } async function _seekInternal (time: number) { @@ -282,12 +287,12 @@ let destroyed = false onDestroy(() => destroyed = true) - async function step () { + function step () { if (destroyed) { return } if (playing) { - await seek(Math.min(duration, timestamp + 0.1)) + seek(Math.min(duration, timestamp + 0.1)) } setTimeout(step, 100) } @@ -334,31 +339,18 @@ bind:this={containerElement} >
-
- -
{ formatDuration(timestamp * 1000, { leading: true }) }
- {#if sessionIsLive === true} - - {/if} - seek(duration * seekInputValue / 100)} /> - -
+
diff --git a/warpgate-web/src/admin/player/latestWins.ts b/warpgate-web/src/admin/player/latestWins.ts new file mode 100644 index 000000000..face2f574 --- /dev/null +++ b/warpgate-web/src/admin/player/latestWins.ts @@ -0,0 +1,47 @@ +// Runs async tasks one at a time, "latest wins": starting a new run supersedes any +// still-queued run (only the most recent pending arg ever executes) and aborts the running +// task's `AbortSignal`, so the task can bail early — and, because it's a standard signal, +// abort in-flight `fetch`es (e.g. a superseded seek's HTTP Range request) for free. +// +// This is the cancel + coalesce + serialize pattern the players kept hand-rolling for +// seeking. Extracted so the terminal and desktop players share one implementation. +export function latestWins (task: (arg: T, signal: AbortSignal) => Promise): (arg: T) => void { + let draining = false + let queued: { arg: T } | null = null + let current: AbortController | null = null + + async function drain (): Promise { + draining = true + try { + while (queued) { + const { arg } = queued + queued = null + current = new AbortController() + const { signal } = current + try { + await task(arg, signal) + } catch (err) { + // An AbortError just means this run was superseded (its fetch / body + // stream was cancelled) — expected, not a failure. `signal.aborted` can + // still read false when the abort arrives via `reader.cancel()`, so also + // match by name. + const aborted = signal.aborted || (err instanceof Error && err.name === 'AbortError') + if (!aborted) { + console.error('latestWins task failed', err) + } + } + } + } finally { + current = null + draining = false + } + } + + return function run (arg: T): void { + current?.abort() // supersede any in-flight task (its signal is now aborted) + queued = { arg } // …and coalesce: only this newest arg will run next + if (!draining) { + void drain() + } + } +} diff --git a/warpgate-web/src/common/ConnectionInstructions.svelte b/warpgate-web/src/common/ConnectionInstructions.svelte index f810a03bd..bc724e61c 100644 --- a/warpgate-web/src/common/ConnectionInstructions.svelte +++ b/warpgate-web/src/common/ConnectionInstructions.svelte @@ -2,7 +2,7 @@ import { Button, FormGroup, ListGroup, ListGroupItem } from '@sveltestrap/sveltestrap' import { api, TargetKind, type ExistingCertificateCredential } from 'gateway/lib/api' import { serverInfo } from 'gateway/lib/store' - import { makeExampleSSHCommand, makeSSHUsername, makeExampleMySQLCommand, makeExampleMySQLURI, makeMySQLUsername, makeTargetURL, makeExamplePostgreSQLCommand, makePostgreSQLUsername, makeExamplePostgreSQLURI, makeKubeconfig, makeExampleKubectlCommand, makeExampleSCPCommand } from 'common/protocols' + import { makeExampleSSHCommand, makeCommonSelectorUsername, makeExampleMySQLCommand, makeExampleMySQLURI, makeMySQLUsername, makeTargetURL, makeExamplePostgreSQLCommand, makePostgreSQLUsername, makeExamplePostgreSQLURI, makeKubeconfig, makeExampleKubectlCommand, makeExampleSCPCommand, protocolHost, protocolPort, protocolPortString } from 'common/protocols' import { getCertificateKey, getAllCertificateKeys } from 'gateway/lib/certificateStore' import CertificateCredentialModal from 'admin/CertificateCredentialModal.svelte' import CopyButton from 'common/CopyButton.svelte' @@ -130,7 +130,7 @@ clientPrivateKeyPem, })) - let sshUsername = $derived(makeSSHUsername(opts)) + let commonSelectorUsername = $derived(makeCommonSelectorUsername(opts)) let exampleSSHCommand = $derived(makeExampleSSHCommand(opts)) let exampleSCPCommand = $derived(makeExampleSCPCommand(opts)) let mySQLUsername = $derived(makeMySQLUsername(opts)) @@ -143,12 +143,14 @@ let authHeader = $derived(`Authorization: Warpgate ${ticketSecret}`) let kubeconfig = $derived(makeKubeconfig(opts)) let exampleKubectlCommand = $derived(makeExampleKubectlCommand(opts)) + let rdpEndpoint = $derived(`${protocolHost(opts, 'rdp')}:${protocolPortString(opts, 'rdp')}`) + let vncEndpoint = $derived(`${protocolHost(opts, 'vnc')}:${protocolPortString(opts, 'vnc')}`) {#if targetKind === TargetKind.Ssh} - - + + @@ -330,3 +332,27 @@ onClose={() => { issuingCertificate = false; loadCertificates() }} /> {/if} + +{#if targetKind === TargetKind.Rdp} + + + + + + + + + +{/if} + +{#if targetKind === TargetKind.Vnc} + + + + + + + + + +{/if} diff --git a/warpgate-web/src/common/desktopCanvas.ts b/warpgate-web/src/common/desktopCanvas.ts new file mode 100644 index 000000000..984c20e06 --- /dev/null +++ b/warpgate-web/src/common/desktopCanvas.ts @@ -0,0 +1,130 @@ +// Shared framebuffer rendering for desktop (RDP/VNC) sessions. +// +// Used by the live in-browser client (gateway/WebDesktop.svelte, synchronous) and the +// admin recording player (admin/player/DesktopRecordingPlayer.svelte, async + ordered so +// image decodes don't race). gen-2 recordings encode framebuffer rects as PNG (`png_image`, +// with `keyframe` full-canvas snapshots); the live interactive client still sends raw BGRA. + +export interface Rect { x: number, y: number, width: number, height: number } + +// Image payloads arrive base64-encoded from recordings (JSON) and as raw bytes from the +// live binary WebSocket; accept either. +// eslint-disable-next-line @typescript-eslint/no-type-alias +type FrameImageData = string | Uint8Array + +/** The visual subset of desktop messages that mutate the framebuffer. */ +export type DesktopFrame = + | { type: 'resize', width: number, height: number } + | { type: 'raw_image', rect: Rect, data: FrameImageData } + | { type: 'png_image', rect: Rect, keyframe?: boolean, data: FrameImageData } + | { type: 'jpeg_image', rect: Rect, data: FrameImageData } + | { type: 'copy_rect', dst: Rect, src_x: number, src_y: number } + | { type: 'cursor', rect: Rect, data: FrameImageData } + +/** + * A frame that only touches part of the surface and can be dropped to catch up + * under load. `resize` and full-frame keyframes are structural and never dropped. + */ +export function isIncrementalFrame (msg: DesktopFrame): boolean { + switch (msg.type) { + case 'raw_image': + case 'jpeg_image': + case 'copy_rect': + case 'cursor': + return true + case 'png_image': + return !msg.keyframe + case 'resize': + return false + } +} + +export function base64ToBytes (b64: string): Uint8Array { + const binary = atob(b64) + const bytes = new Uint8Array(binary.length) + for (let i = 0; i < binary.length; i++) { + bytes[i] = binary.charCodeAt(i) + } + return bytes +} + +/** Normalize an image payload (base64 from recordings, raw bytes from the live WS). */ +function toBytes (data: FrameImageData): Uint8Array { + return typeof data === 'string' ? base64ToBytes(data) : data +} + +export function ensureCanvasSize (canvas: HTMLCanvasElement, width: number, height: number): void { + if (canvas.width !== width || canvas.height !== height) { + canvas.width = width + canvas.height = height + } +} + +function ensureForRect (canvas: HTMLCanvasElement, rect: Rect): void { + ensureCanvasSize( + canvas, + Math.max(canvas.width, rect.x + rect.width), + Math.max(canvas.height, rect.y + rect.height), + ) +} + +function drawRaw (ctx: CanvasRenderingContext2D, rect: Rect, bgra: Uint8Array): void { + const count = rect.width * rect.height + const rgba = new Uint8ClampedArray(count * 4) + for (let i = 0; i < count; i++) { + const s = i * 4 + // server sends BGRA, canvas wants RGBA + rgba[s] = bgra[s + 2] ?? 0 + rgba[s + 1] = bgra[s + 1] ?? 0 + rgba[s + 2] = bgra[s] ?? 0 + rgba[s + 3] = 255 + } + ctx.putImageData(new ImageData(rgba, rect.width, rect.height), rect.x, rect.y) +} + +async function drawImageBlob ( + ctx: CanvasRenderingContext2D, + rect: Rect, + bytes: Uint8Array, + mime: string, +): Promise { + const bitmap = await createImageBitmap(new Blob([bytes], { type: mime })) + ctx.drawImage(bitmap, rect.x, rect.y) + bitmap.close() +} + +/** Apply one framebuffer message. Awaiting the result renders frames strictly in order + * (recording player: a keyframe must fully paint before the deltas that follow it); the + * live client fire-and-forgets it (`void`), matching single-frame-at-a-time streaming. */ +export async function applyDesktopFrame ( + canvas: HTMLCanvasElement, + ctx: CanvasRenderingContext2D, + msg: DesktopFrame, +): Promise { + switch (msg.type) { + case 'resize': + ensureCanvasSize(canvas, msg.width, msg.height) + break + case 'raw_image': + ensureForRect(canvas, msg.rect) + drawRaw(ctx, msg.rect, toBytes(msg.data)) + break + case 'png_image': + ensureForRect(canvas, msg.rect) + await drawImageBlob(ctx, msg.rect, toBytes(msg.data), 'image/png') + break + case 'jpeg_image': + ensureForRect(canvas, msg.rect) + await drawImageBlob(ctx, msg.rect, toBytes(msg.data), 'image/jpeg') + break + case 'copy_rect': + ctx.drawImage( + canvas, + msg.src_x, msg.src_y, msg.dst.width, msg.dst.height, + msg.dst.x, msg.dst.y, msg.dst.width, msg.dst.height, + ) + break + case 'cursor': + break + } +} diff --git a/warpgate-web/src/common/desktopInput.ts b/warpgate-web/src/common/desktopInput.ts new file mode 100644 index 000000000..ceef05daf --- /dev/null +++ b/warpgate-web/src/common/desktopInput.ts @@ -0,0 +1,63 @@ +// Decoding viewer-input recording items into human-readable key labels and click +// positions, for the recording player's live-input overlay. Keyboard input arrives +// two ways: X11 keysyms (VNC path) and raw PC/AT set-1 scancodes (native RDP path). + +// Hex keys are intentional (they mirror the wire values); quoting them would change +// the property name, so keep them as numeric literals. +/* eslint-disable quote-props */ + +export interface KeyPress { time: number, label: string } +export interface Click { time: number, x: number, y: number } + +// Named X11 keysyms (non-printable keys). Printable Latin-1 keysyms equal their +// Unicode code point, so they fall through to `String.fromCharCode` below. +const KEYSYM_NAMES: Record = { + 0x20: 'Space', + 0xff08: 'Backspace', 0xff09: 'Tab', 0xff0d: 'Enter', 0xff1b: 'Esc', + 0xff50: 'Home', 0xff51: '←', 0xff52: '↑', 0xff53: '→', 0xff54: '↓', + 0xff55: 'PgUp', 0xff56: 'PgDn', 0xff57: 'End', 0xff63: 'Insert', 0xffff: 'Delete', + 0xffe1: 'Shift', 0xffe2: 'Shift', 0xffe3: 'Ctrl', 0xffe4: 'Ctrl', + 0xffe5: 'CapsLock', 0xffe9: 'Alt', 0xffea: 'Alt', 0xffeb: 'Super', 0xffec: 'Super', + 0xffbe: 'F1', 0xffbf: 'F2', 0xffc0: 'F3', 0xffc1: 'F4', 0xffc2: 'F5', 0xffc3: 'F6', + 0xffc4: 'F7', 0xffc5: 'F8', 0xffc6: 'F9', 0xffc7: 'F10', 0xffc8: 'F11', 0xffc9: 'F12', +} + +export function keysymLabel (keysym: number): string { + const named = KEYSYM_NAMES[keysym] + if (named) { return named } + if (keysym >= 0x21 && keysym <= 0xff) { return String.fromCharCode(keysym) } + // Native RDP sends Unicode code points on its key path. + try { + const s = String.fromCodePoint(keysym) + if (s.trim()) { return s } + } catch { /* invalid code point */ } + return `0x${keysym.toString(16)}` +} + +// PC/AT set-1 "make" codes. The nav cluster (arrows/Home/End/…) shares codes with +// the keypad; the `extended` flag disambiguates, but the labels are the same either +// way, so we don't need it here. +const SCANCODE_NAMES: Record = { + 0x01: 'Esc', + 0x02: '1', 0x03: '2', 0x04: '3', 0x05: '4', 0x06: '5', + 0x07: '6', 0x08: '7', 0x09: '8', 0x0a: '9', 0x0b: '0', + 0x0c: '-', 0x0d: '=', 0x0e: 'Backspace', 0x0f: 'Tab', + 0x10: 'Q', 0x11: 'W', 0x12: 'E', 0x13: 'R', 0x14: 'T', 0x15: 'Y', + 0x16: 'U', 0x17: 'I', 0x18: 'O', 0x19: 'P', 0x1a: '[', 0x1b: ']', 0x1c: 'Enter', + 0x1d: 'Ctrl', + 0x1e: 'A', 0x1f: 'S', 0x20: 'D', 0x21: 'F', 0x22: 'G', 0x23: 'H', + 0x24: 'J', 0x25: 'K', 0x26: 'L', 0x27: ';', 0x28: '\'', 0x29: '`', + 0x2a: 'Shift', 0x2b: '\\', + 0x2c: 'Z', 0x2d: 'X', 0x2e: 'C', 0x2f: 'V', 0x30: 'B', 0x31: 'N', + 0x32: 'M', 0x33: ',', 0x34: '.', 0x35: '/', 0x36: 'Shift', 0x37: '*', + 0x38: 'Alt', 0x39: 'Space', 0x3a: 'CapsLock', + 0x3b: 'F1', 0x3c: 'F2', 0x3d: 'F3', 0x3e: 'F4', 0x3f: 'F5', 0x40: 'F6', + 0x41: 'F7', 0x42: 'F8', 0x43: 'F9', 0x44: 'F10', 0x57: 'F11', 0x58: 'F12', + 0x45: 'NumLock', 0x46: 'ScrollLock', + 0x47: 'Home', 0x48: '↑', 0x49: 'PgUp', 0x4b: '←', 0x4d: '→', + 0x4f: 'End', 0x50: '↓', 0x51: 'PgDn', 0x52: 'Insert', 0x53: 'Delete', +} + +export function scancodeLabel (code: number): string { + return SCANCODE_NAMES[code] ?? `0x${code.toString(16)}` +} diff --git a/warpgate-web/src/common/protocols.ts b/warpgate-web/src/common/protocols.ts index 693be6e0c..c88864892 100644 --- a/warpgate-web/src/common/protocols.ts +++ b/warpgate-web/src/common/protocols.ts @@ -13,39 +13,33 @@ export interface ConnectionOptions { clientPrivateKeyPem?: string } -export function makeSSHUsername (opt: ConnectionOptions): string { +export function makeCommonSelectorUsername (opt: ConnectionOptions): string { if (opt.ticketSecret) { return `ticket-${opt.ticketSecret}` } return `${opt.username ?? 'username'}:${opt.targetName ?? 'target'}` } -function protocolHost (opt: ConnectionOptions, protocol: 'ssh'|'http'|'mysql'|'postgres'|'kubernetes'): string { +export function protocolHost (opt: ConnectionOptions, protocol: 'ssh'|'http'|'mysql'|'postgres'|'kubernetes'|'rdp'|'vnc'): string { const globalHost = opt.serverInfo?.externalHost ?? 'warpgate-host' const hosts = opt.serverInfo?.externalHosts + return hosts?.[protocol] ?? opt.targetExternalHost ?? globalHost +} - switch (protocol) { - case 'ssh': - return hosts?.ssh ?? globalHost - case 'http': - return hosts?.http ?? globalHost - case 'mysql': - return hosts?.mysql ?? globalHost - case 'postgres': - return hosts?.postgres ?? globalHost - case 'kubernetes': - return hosts?.kubernetes ?? globalHost - default: - return globalHost - } +export function protocolPort (opt: ConnectionOptions, protocol: 'ssh'|'http'|'mysql'|'postgres'|'kubernetes'|'rdp'|'vnc'): number | undefined { + return opt.serverInfo?.ports[protocol] +} + +export function protocolPortString (opt: ConnectionOptions, protocol: 'ssh'|'http'|'mysql'|'postgres'|'kubernetes'|'rdp'|'vnc'): string { + return protocolPort(opt, protocol)?.toString() ?? `warpgate-${protocol}-port` } export function makeExampleSSHCommand (opt: ConnectionOptions): string { return shellEscape([ 'ssh', - `${makeSSHUsername(opt)}@${protocolHost(opt, 'ssh')}`, + `${makeCommonSelectorUsername(opt)}@${protocolHost(opt, 'ssh')}`, '-p', - (opt.serverInfo?.ports.ssh ?? 'warpgate-ssh-port').toString(), + protocolPortString(opt, 'ssh'), ]) } @@ -53,9 +47,9 @@ export function makeExampleSCPCommand (opt: ConnectionOptions): string { return shellEscape([ 'scp', '-o', - `User="${makeSSHUsername(opt)}"`, + `User="${makeCommonSelectorUsername(opt)}"`, '-P', - (opt.serverInfo?.ports.ssh ?? 'warpgate-ssh-port').toString(), + protocolPortString(opt, 'ssh'), 'local-file', `${protocolHost(opt, 'ssh')}:remote-file`, ]) @@ -70,7 +64,15 @@ export function makeMySQLUsername (opt: ConnectionOptions): string { export function makeExampleMySQLCommand (opt: ConnectionOptions): string { const dbName = opt.targetDefaultDatabaseName?.trim() || 'database-name' - let cmd = shellEscape(['mysql', '-u', makeMySQLUsername(opt), '--host', protocolHost(opt, 'mysql'), '--port', (opt.serverInfo?.ports.mysql ?? 'warpgate-mysql-port').toString(), '--ssl', dbName]) + let cmd = shellEscape([ + 'mysql', + '-u', makeMySQLUsername(opt), + '--host', protocolHost(opt, 'mysql'), + '--port', + protocolPortString(opt, 'mysql'), + '--ssl', + dbName, + ]) if (!opt.ticketSecret) { cmd += ' -p' } @@ -80,14 +82,14 @@ export function makeExampleMySQLCommand (opt: ConnectionOptions): string { export function makeExampleMySQLURI (opt: ConnectionOptions): string { const pwSuffix = opt.ticketSecret ? '' : ':' const dbName = opt.targetDefaultDatabaseName?.trim() || 'database-name' - return `mysql://${makeMySQLUsername(opt)}${pwSuffix}@${protocolHost(opt, 'mysql')}:${opt.serverInfo?.ports.mysql ?? 'warpgate-mysql-port'}/${dbName}?sslMode=required` + return `mysql://${makeMySQLUsername(opt)}${pwSuffix}@${protocolHost(opt, 'mysql')}:${protocolPortString(opt, 'mysql')}/${dbName}?sslMode=required` } export const makePostgreSQLUsername = makeMySQLUsername export function makeExamplePostgreSQLCommand (opt: ConnectionOptions): string { const dbName = opt.targetDefaultDatabaseName?.trim() || 'database-name' - const args = ['psql', '-U', makeMySQLUsername(opt), '--host', protocolHost(opt, 'postgres'), '--port', (opt.serverInfo?.ports.postgres ?? 'warpgate-postgres-port').toString()] + const args = ['psql', '-U', makeMySQLUsername(opt), '--host', protocolHost(opt, 'postgres'), '--port', protocolPortString(opt, 'postgres')] if (!opt.ticketSecret) { args.push('-W') } @@ -98,11 +100,11 @@ export function makeExamplePostgreSQLCommand (opt: ConnectionOptions): string { export function makeExamplePostgreSQLURI (opt: ConnectionOptions): string { const pwSuffix = opt.ticketSecret ? '' : ':' const dbName = opt.targetDefaultDatabaseName?.trim() || 'database-name' - return `postgresql://${makePostgreSQLUsername(opt)}${pwSuffix}@${protocolHost(opt, 'postgres')}:${opt.serverInfo?.ports.postgres ?? 'warpgate-postgres-port'}/${dbName}?sslmode=require` + return `postgresql://${makePostgreSQLUsername(opt)}${pwSuffix}@${protocolHost(opt, 'postgres')}:${protocolPortString(opt, 'postgres')}/${dbName}?sslmode=require` } export function makeTargetURL (opt: ConnectionOptions): string { - const host = `${opt.targetExternalHost ?? protocolHost(opt, 'http')}:${opt.serverInfo?.ports.http ?? 443}` + const host = `${opt.targetExternalHost ?? protocolHost(opt, 'http')}:${protocolPort(opt, 'http') ?? 443}` if (opt.ticketSecret) { return `${location.protocol}//${host}/?warpgate-ticket=${opt.ticketSecret}` @@ -116,6 +118,9 @@ export const possibleCredentials: Record> = { mysql: new Set([CredentialKind.Password]), postgres: new Set([CredentialKind.Password, CredentialKind.WebUserApproval]), kubernetes: new Set([CredentialKind.Certificate, CredentialKind.WebUserApproval]), + vnc: new Set([CredentialKind.Password, CredentialKind.Totp, CredentialKind.WebUserApproval]), + // Password over NLA, then TOTP / web approval gathered on the holding screen. + rdp: new Set([CredentialKind.Password, CredentialKind.Totp, CredentialKind.WebUserApproval]), } export function abbreviatePublicKey (key: string): string { @@ -134,7 +139,7 @@ export function makeKubernetesNamespace (_opt: ConnectionOptions): string { } export function makeKubernetesClusterUrl (opt: ConnectionOptions): string { - const baseUrl = `https://${protocolHost(opt, 'kubernetes')}:${opt.serverInfo?.ports.kubernetes ?? 'warpgate-kubernetes-port'}` + const baseUrl = `https://${protocolHost(opt, 'kubernetes')}:${protocolPortString(opt, 'kubernetes')}` return `${baseUrl}/${encodeURIComponent(opt.targetName ?? 'target')}` } @@ -204,4 +209,6 @@ export const PROTOCOL_PROPERTIES: Record = { MySQL: { sessionsCanBeClosed: true }, PostgreSQL: { sessionsCanBeClosed: true }, Kubernetes: { sessionsCanBeClosed: false }, + VNC: { sessionsCanBeClosed: true }, + RDP: { sessionsCanBeClosed: true }, } diff --git a/warpgate-web/src/common/reauth.ts b/warpgate-web/src/common/reauth.ts index 6906b69b4..cbab17081 100644 --- a/warpgate-web/src/common/reauth.ts +++ b/warpgate-web/src/common/reauth.ts @@ -7,7 +7,14 @@ export async function handleReauthError (err: unknown): Promise { if (err instanceof ResponseError && err.response.status === 401) { // If we don't cancel the current AuthState, the server // will just go 'yup, you're logged in aight' - await api.cancelDefaultAuth() + try { + await api.cancelDefaultAuth() + } catch (e) { + if (!(e instanceof ResponseError && e.response.status === 404)) { + // 404 if default auth state is already cleared + throw e + } + } const next = location.pathname + location.hash location.assign('/@warpgate#/login?next=' + encodeURIComponent(next) + '&reauth=1') diff --git a/warpgate-web/src/common/recordings.ts b/warpgate-web/src/common/recordings.ts index 2495a0d31..85a88b58e 100644 --- a/warpgate-web/src/common/recordings.ts +++ b/warpgate-web/src/common/recordings.ts @@ -33,6 +33,10 @@ export type RecordingMetadata ={ } | { type: 'ssh-forwarded-socket', path: string +} | { + type: 'desktop', + protocol: string + target: string } @@ -66,6 +70,8 @@ export function recordingMetadataToFieldSet(metadata: RecordingMetadata): [strin case 'ssh-forwarded-socket': fieldSets.push(['Path', metadata.path]) break + case 'desktop': + break } return fieldSets @@ -92,6 +98,8 @@ export function recordingTypeLabel(recording: Recording): string { return 'Remote TCP forwarding' case 'ssh-forwarded-socket': return 'Remote UNIX socket forwarding' + case 'desktop': + return 'Desktop' } return 'Unknown type' diff --git a/warpgate-web/src/gateway/Root.svelte b/warpgate-web/src/gateway/Root.svelte index 76da24268..72965dbd1 100644 --- a/warpgate-web/src/gateway/Root.svelte +++ b/warpgate-web/src/gateway/Root.svelte @@ -6,6 +6,9 @@ '/web-ssh/:sessionId': wrap({ asyncComponent: () => import('./WebSsh.svelte') as any, }), + '/web-desktop/:sessionId': wrap({ + asyncComponent: () => import('./WebDesktop.svelte') as any, + }), '/': wrap({ asyncComponent: () => import('./App.svelte') as any, }), diff --git a/warpgate-web/src/gateway/TargetList.svelte b/warpgate-web/src/gateway/TargetList.svelte index 03fcce47b..e37e5ff0a 100644 --- a/warpgate-web/src/gateway/TargetList.svelte +++ b/warpgate-web/src/gateway/TargetList.svelte @@ -17,7 +17,7 @@ import { handleReauthError } from 'common/reauth' let instructionsTarget: TargetSnapshot|undefined = $state() const canEditTargets = $derived($serverInfo?.adminPermissions?.targetsEdit ?? false) -const webSshEnabled = $derived($serverInfo?.webSshEnabled ?? true) +const webClientsEnabled = $derived($serverInfo?.webClientsEnabled ?? true) async function openWebSsh (target: TargetSnapshot) { try { @@ -32,6 +32,19 @@ async function openWebSsh (target: TargetSnapshot) { } } +async function openWebDesktop (target: TargetSnapshot) { + try { + const { sessionId } = await api.createWebDesktopSession({ + createWebDesktopSessionBody: { targetId: target.id }, + }) + window.open(`/@warpgate#/web-desktop/${sessionId}`, '_blank') + } catch (err) { + if (!(await handleReauthError(err))) { + throw err + } + } +} + function loadTargets( options: LoadOptions ): Observable> { @@ -76,11 +89,17 @@ function selectTarget (target: TargetSnapshot) { } } else if (target.kind === TargetKind.Ssh) { const targetClickAction = $serverInfo?.targetClickAction - if (!webSshEnabled || targetClickAction === TargetClickAction.ShowInstructions) { + if (!webClientsEnabled || targetClickAction === TargetClickAction.ShowInstructions) { instructionsTarget = target } else { openWebSsh(target) } + } else if (target.kind === TargetKind.Vnc || target.kind === TargetKind.Rdp) { + if (!webClientsEnabled) { + instructionsTarget = target + } else { + openWebDesktop(target) + } } else { instructionsTarget = target } @@ -173,48 +192,53 @@ function groupInfoFromTarget (target: TargetSnapshot): GroupInfo { {#if target.kind === TargetKind.Ssh} SSH {/if} + {#if target.kind === TargetKind.Vnc} + VNC + {/if} + {#if target.kind === TargetKind.Rdp} + RDP + {/if} {#if target.kind === TargetKind.Http} {/if} - {#if target.kind === TargetKind.Ssh || canEditTargets} - - { + + { + e.preventDefault() + e.stopPropagation() + }}> + + + + {#if target.kind === TargetKind.Ssh && webClientsEnabled} + { + openWebSsh(target) + e.preventDefault() + e.stopPropagation() + }}>Web terminal + {/if} + {#if (target.kind === TargetKind.Vnc || target.kind === TargetKind.Rdp) && webClientsEnabled} + { + openWebDesktop(target) + e.preventDefault() + e.stopPropagation() + }}>Web desktop + {/if} + { + showInstructions(target) e.preventDefault() e.stopPropagation() - }}> - - - - {#if target.kind === TargetKind.Ssh} - {#if webSshEnabled} - { - openWebSsh(target) - e.preventDefault() - e.stopPropagation() - }}>Web terminal - {/if} - { - showInstructions(target) - e.preventDefault() - e.stopPropagation() - }}>Connection instructions - {/if} - {#if canEditTargets} - e.stopPropagation()} - >Edit target - {/if} - - - {:else if target.kind !== TargetKind.Http} - - {/if} + }}>Connection instructions + {#if canEditTargets} + e.stopPropagation()} + >Edit target + {/if} + + {/snippet} @@ -239,7 +263,7 @@ function groupInfoFromTarget (target: TargetSnapshot): GroupInfo { {/if} - {#if instructionsTarget?.kind === TargetKind.Ssh && webSshEnabled} + {#if instructionsTarget?.kind === TargetKind.Ssh && webClientsEnabled} +
+ + {#if connectionError} +
+ + {#if sessionNotFound} + Session not found. It may have expired or been closed. + {:else} + {connectionError} + {/if} + +
+ {/if} + +
+ e.preventDefault()} + > +
+ + + diff --git a/warpgate-web/src/gateway/WebSsh.svelte b/warpgate-web/src/gateway/WebSsh.svelte index 26deaed3c..10aeaea08 100644 --- a/warpgate-web/src/gateway/WebSsh.svelte +++ b/warpgate-web/src/gateway/WebSsh.svelte @@ -77,7 +77,7 @@ const ws = new ReconnectingWebSocket({ url: `wss://${location.host}/@warpgate/api/web-ssh/sessions/${sessionId}/stream`, onOpen: () => requestNewChannel(), - onMessage: data => onMessage(JSON.parse(data) as ServerMessage), + onMessage: data => onMessage(JSON.parse(data as string) as ServerMessage), }) function send (msg: ClientMessage) { diff --git a/warpgate-web/src/gateway/lib/ReconnectingWebSocket.svelte.ts b/warpgate-web/src/gateway/lib/ReconnectingWebSocket.svelte.ts index cd22fc861..74d49e1c9 100644 --- a/warpgate-web/src/gateway/lib/ReconnectingWebSocket.svelte.ts +++ b/warpgate-web/src/gateway/lib/ReconnectingWebSocket.svelte.ts @@ -8,7 +8,7 @@ export enum ConnectionState { export interface ReconnectingWebSocketOptions { url: string onOpen: () => void - onMessage: (data: string) => void + onMessage: (data: string | ArrayBuffer) => void } export class ReconnectingWebSocket { @@ -20,7 +20,7 @@ export class ReconnectingWebSocket { private closed = false private readonly url: string private readonly onOpen: () => void - private readonly onMessage: (data: string) => void + private readonly onMessage: (data: string | ArrayBuffer) => void private readonly maxAttempts = 5 constructor (opts: ReconnectingWebSocketOptions) { @@ -34,6 +34,8 @@ export class ReconnectingWebSocket { return } this.socket = new WebSocket(this.url) + // Framebuffer frames arrive as binary; get them as ArrayBuffer, not Blob. + this.socket.binaryType = 'arraybuffer' this.socket.addEventListener('open', () => { this.attempt = 0 @@ -42,7 +44,7 @@ export class ReconnectingWebSocket { }) this.socket.addEventListener('message', e => { - this.onMessage(e.data as string) + this.onMessage(e.data as string | ArrayBuffer) }) this.socket.addEventListener('error', () => { diff --git a/warpgate-web/src/gateway/lib/openapi-schema.json b/warpgate-web/src/gateway/lib/openapi-schema.json index 1d87ae61e..b9cc82379 100644 --- a/warpgate-web/src/gateway/lib/openapi-schema.json +++ b/warpgate-web/src/gateway/lib/openapi-schema.json @@ -2,7 +2,7 @@ "openapi": "3.0.0", "info": { "title": "Warpgate HTTP proxy", - "version": "v0.26.0-2-g81553667-modified" + "version": "v0.26.0-35-g86c7d219-modified" }, "servers": [ { @@ -1286,6 +1286,129 @@ ], "operationId": "delete_web_ssh_session" } + }, + "/web-desktop/sessions": { + "post": { + "requestBody": { + "content": { + "application/json; charset=utf-8": { + "schema": { + "$ref": "#/components/schemas/CreateWebDesktopSessionBody" + } + } + }, + "required": true + }, + "responses": { + "201": { + "description": "", + "content": { + "application/json; charset=utf-8": { + "schema": { + "$ref": "#/components/schemas/WebDesktopSessionCreated" + } + } + } + }, + "401": { + "description": "" + }, + "403": { + "description": "" + }, + "404": { + "description": "" + }, + "429": { + "description": "" + } + }, + "security": [ + { + "TokenSecurityScheme": [] + }, + { + "CookieSecurityScheme": [] + } + ], + "operationId": "create_web_desktop_session" + } + }, + "/web-desktop/sessions/{session_id}": { + "get": { + "parameters": [ + { + "name": "session_id", + "schema": { + "type": "string", + "format": "uuid" + }, + "in": "path", + "required": true, + "deprecated": false, + "explode": true + } + ], + "responses": { + "200": { + "description": "", + "content": { + "application/json; charset=utf-8": { + "schema": { + "$ref": "#/components/schemas/WebDesktopSessionInfo" + } + } + } + }, + "404": { + "description": "" + } + }, + "security": [ + { + "TokenSecurityScheme": [] + }, + { + "CookieSecurityScheme": [] + } + ], + "operationId": "get_web_desktop_session" + }, + "delete": { + "parameters": [ + { + "name": "session_id", + "schema": { + "type": "string", + "format": "uuid" + }, + "in": "path", + "required": true, + "deprecated": false, + "explode": true + } + ], + "responses": { + "204": { + "description": "" + }, + "403": { + "description": "" + }, + "404": { + "description": "" + } + }, + "security": [ + { + "TokenSecurityScheme": [] + }, + { + "CookieSecurityScheme": [] + } + ], + "operationId": "delete_web_desktop_session" + } } }, "components": { @@ -1477,6 +1600,19 @@ } } }, + "CreateWebDesktopSessionBody": { + "type": "object", + "title": "CreateWebDesktopSessionBody", + "required": [ + "target_id" + ], + "properties": { + "target_id": { + "type": "string", + "format": "uuid" + } + } + }, "CreateWebSshSessionBody": { "type": "object", "title": "CreateWebSshSessionBody", @@ -1704,6 +1840,12 @@ }, "kubernetes": { "type": "string" + }, + "vnc": { + "type": "string" + }, + "rdp": { + "type": "string" } } }, @@ -1741,7 +1883,7 @@ "ticket_require_description", "ticket_request_show_all_targets", "target_click_action", - "web_ssh_enabled", + "web_clients_enabled", "has_ldap", "should_prompt_analytics" ], @@ -1804,7 +1946,7 @@ "type": "integer", "format": "int64" }, - "web_ssh_enabled": { + "web_clients_enabled": { "type": "boolean" }, "has_ldap": { @@ -2066,6 +2208,14 @@ "kubernetes": { "type": "integer", "format": "uint16" + }, + "vnc": { + "type": "integer", + "format": "uint16" + }, + "rdp": { + "type": "integer", + "format": "uint16" } } }, @@ -2156,7 +2306,9 @@ "Kubernetes", "MySql", "Ssh", - "Postgres" + "Postgres", + "Vnc", + "Rdp" ] }, "TargetSnapshot": { @@ -2397,6 +2549,47 @@ "items": { "$ref": "#/components/schemas/CredentialKind" } + }, + "vnc": { + "type": "array", + "items": { + "$ref": "#/components/schemas/CredentialKind" + } + }, + "rdp": { + "type": "array", + "items": { + "$ref": "#/components/schemas/CredentialKind" + } + } + } + }, + "WebDesktopSessionCreated": { + "type": "object", + "title": "WebDesktopSessionCreated", + "required": [ + "session_id" + ], + "properties": { + "session_id": { + "type": "string", + "format": "uuid" + } + } + }, + "WebDesktopSessionInfo": { + "type": "object", + "title": "WebDesktopSessionInfo", + "required": [ + "target_name", + "target_kind" + ], + "properties": { + "target_name": { + "type": "string" + }, + "target_kind": { + "$ref": "#/components/schemas/TargetKind" } } }, diff --git a/warpgate/Cargo.toml b/warpgate/Cargo.toml index 848fb87a4..43086af20 100644 --- a/warpgate/Cargo.toml +++ b/warpgate/Cargo.toml @@ -55,7 +55,9 @@ warpgate-protocol-http = { path = "../warpgate-protocol-http" } warpgate-protocol-kubernetes = { path = "../warpgate-protocol-kubernetes" } warpgate-protocol-mysql = { path = "../warpgate-protocol-mysql" } warpgate-protocol-postgres = { path = "../warpgate-protocol-postgres" } +warpgate-protocol-rdp = { path = "../warpgate-protocol-rdp" } warpgate-protocol-ssh = { path = "../warpgate-protocol-ssh" } +warpgate-protocol-vnc = { path = "../warpgate-protocol-vnc" } warpgate-tls = { path = "../warpgate-tls" } [dev-dependencies] diff --git a/warpgate/src/commands/run.rs b/warpgate/src/commands/run.rs index 8fa934381..643b4b7d5 100644 --- a/warpgate/src/commands/run.rs +++ b/warpgate/src/commands/run.rs @@ -19,7 +19,9 @@ use warpgate_protocol_http::HTTPProtocolServer; use warpgate_protocol_kubernetes::KubernetesProtocolServer; use warpgate_protocol_mysql::MySQLProtocolServer; use warpgate_protocol_postgres::PostgresProtocolServer; +use warpgate_protocol_rdp::RdpProtocolServer; use warpgate_protocol_ssh::SSHProtocolServer; +use warpgate_protocol_vnc::VncProtocolServer; use crate::config::{load_config, watch_config}; use crate::listener_supervisor::{ @@ -168,8 +170,18 @@ pub async fn command(params: &GlobalParams, enable_admin_token: bool) -> Result< } supervisors.push(tls_listener!("MySQL", MySQLProtocolServer, mysql)); - supervisors.push(tls_listener!("PostgreSQL", PostgresProtocolServer, postgres)); - supervisors.push(tls_listener!("Kubernetes", KubernetesProtocolServer, kubernetes)); + supervisors.push(tls_listener!( + "PostgreSQL", + PostgresProtocolServer, + postgres + )); + supervisors.push(tls_listener!( + "Kubernetes", + KubernetesProtocolServer, + kubernetes + )); + supervisors.push(tls_listener!("VNC", VncProtocolServer, vnc)); + supervisors.push(tls_listener!("RDP", RdpProtocolServer, rdp)); tokio::spawn({ let services = services.clone(); diff --git a/warpgate/src/commands/setup.rs b/warpgate/src/commands/setup.rs index 023fc68cd..8bd2b38c1 100644 --- a/warpgate/src/commands/setup.rs +++ b/warpgate/src/commands/setup.rs @@ -16,7 +16,7 @@ use warpgate_common::helpers::fs::{secure_directory, secure_file}; use warpgate_common::version::warpgate_version; use warpgate_common::{ GlobalParams, HttpConfig, KubernetesConfig, ListenEndpoint, MySqlConfig, PostgresConfig, - Secret, SshConfig, WarpgateConfigStore, + RdpConfig, Secret, SshConfig, VncConfig, WarpgateConfigStore, }; use warpgate_core::consts::{BUILTIN_ADMIN_ROLE_NAME, BUILTIN_ADMIN_USERNAME}; use warpgate_core::db::connect_to_db_and_migrate; @@ -255,6 +255,52 @@ pub async fn command(cli: &Cli, params: &GlobalParams) -> Result<()> { } } + // VNC and RDP native listeners are off by default (browser access needs no listener, + // and the native path is newer); enable explicitly if requested. + if let Commands::UnattendedSetup { vnc_port, .. } = &cli.command { + if let Some(vnc_port) = vnc_port { + store.vnc.enable = true; + store.vnc.listen = + ListenEndpoint::from(SocketAddr::new(Ipv6Addr::UNSPECIFIED.into(), *vnc_port)); + } + } else { + if !is_docker() { + store.vnc.enable = dialoguer::Confirm::with_theme(&theme) + .default(false) + .with_prompt("Accept VNC connections?") + .interact()?; + + if store.vnc.enable { + store.vnc.listen = prompt_endpoint( + "Endpoint to listen for VNC connections on", + &VncConfig::default().listen, + ); + } + } + } + + if let Commands::UnattendedSetup { rdp_port, .. } = &cli.command { + if let Some(rdp_port) = rdp_port { + store.rdp.enable = true; + store.rdp.listen = + ListenEndpoint::from(SocketAddr::new(Ipv6Addr::UNSPECIFIED.into(), *rdp_port)); + } + } else { + if !is_docker() { + store.rdp.enable = dialoguer::Confirm::with_theme(&theme) + .default(false) + .with_prompt("Accept RDP connections?") + .interact()?; + + if store.rdp.enable { + store.rdp.listen = prompt_endpoint( + "Endpoint to listen for RDP connections on", + &RdpConfig::default().listen, + ); + } + } + } + store.http.certificate = data_path .join("tls.certificate.pem") .to_string_lossy() @@ -271,6 +317,12 @@ pub async fn command(cli: &Cli, params: &GlobalParams) -> Result<()> { store.kubernetes.certificate = store.http.certificate.clone(); store.kubernetes.key = store.http.key.clone(); + store.vnc.certificate = store.http.certificate.clone(); + store.vnc.key = store.http.key.clone(); + + store.rdp.certificate = store.http.certificate.clone(); + store.rdp.key = store.http.key.clone(); + // --- store.ssh.keys = data_path.join("ssh-keys").to_string_lossy().to_string(); diff --git a/warpgate/src/main.rs b/warpgate/src/main.rs index 4d6d7b21f..19e42b829 100644 --- a/warpgate/src/main.rs +++ b/warpgate/src/main.rs @@ -80,6 +80,14 @@ pub(crate) enum Commands { #[clap(long)] kubernetes_port: Option, + /// Enable VNC and set port + #[clap(long)] + vnc_port: Option, + + /// Enable RDP and set port + #[clap(long)] + rdp_port: Option, + /// Enable session recording #[clap(long)] record_sessions: bool, From eaea08e8f526e0773febb77745cb8dc7012c20fa Mon Sep 17 00:00:00 2001 From: mba965 <268970789+mba965@users.noreply.github.com> Date: Sun, 5 Jul 2026 22:27:29 +0200 Subject: [PATCH 257/556] fix(auth): avoid holding the AuthStateStore lock during DB I/O and serialization (#2141) Co-authored-by: Eugene --- .github/workflows/test.yml | 6 +- warpgate-core/src/auth_state_store.rs | 158 +++++++------- warpgate-core/src/services.rs | 40 +++- warpgate-desktop-auth/src/lib.rs | 8 +- warpgate-protocol-http/src/api/auth.rs | 97 +++++++-- warpgate-protocol-http/src/common.rs | 6 +- warpgate-protocol-mysql/src/session.rs | 8 +- warpgate-protocol-postgres/src/session.rs | 8 +- warpgate-protocol-ssh/src/server/session.rs | 9 +- warpgate-web/package-lock.json | 29 ++- warpgate-web/package.json | 2 +- .../src/admin/AnalyticsConsentModal.svelte | 3 +- .../src/admin/AnalyticsPreview.svelte | 2 +- .../admin/CertificateCredentialModal.svelte | 2 +- .../src/admin/CreatePasswordModal.svelte | 2 +- .../src/admin/KubernetesRecording.svelte | 2 +- warpgate-web/src/admin/Recording.svelte | 2 +- warpgate-web/src/admin/Session.svelte | 6 +- .../src/admin/SsoCredentialModal.svelte | 2 +- .../src/admin/config/AccessRole.svelte | 2 +- .../src/admin/config/AdminRole.svelte | 4 +- .../config/AdminRolePermissionsBadge.svelte | 4 +- .../src/admin/config/AdminRoles.svelte | 2 +- .../src/admin/config/CreateAdminRole.svelte | 3 +- .../src/admin/config/CreateRole.svelte | 3 +- .../src/admin/config/CreateTicket.svelte | 3 +- .../src/admin/config/CreateUser.svelte | 3 +- .../src/admin/config/LoginProtection.svelte | 3 +- .../src/admin/config/Parameters.svelte | 3 +- warpgate-web/src/admin/config/SSHKeys.svelte | 3 +- warpgate-web/src/admin/config/Tickets.svelte | 3 +- .../admin/config/ldap/CreateLdapServer.svelte | 3 +- .../admin/config/ldap/LdapUserBrowser.svelte | 2 +- .../target-groups/CreateTargetGroup.svelte | 3 +- .../config/target-groups/TargetGroup.svelte | 3 +- .../config/targets/ChooseTargetKind.svelte | 2 +- .../admin/config/targets/CreateTarget.svelte | 3 +- .../src/admin/config/targets/Target.svelte | 4 +- .../src/admin/config/targets/Targets.svelte | 3 +- .../config/targets/ssh/KeyChecker.svelte | 2 +- .../targets/ssh/KeyCheckerResult.svelte | 2 +- .../admin/config/targets/ssh/Options.svelte | 3 +- .../config/users/CredentialEditor.svelte | 3 +- .../src/admin/config/users/User.svelte | 9 +- .../src/admin/lib/PermissionGate.svelte | 2 +- .../admin/log-viewer/AccessRoleBadge.svelte | 2 +- .../admin/log-viewer/AdminRoleBadge.svelte | 2 +- .../src/admin/log-viewer/LogViewer.svelte | 3 +- .../src/admin/log-viewer/RoleBadge.svelte | 2 +- .../src/admin/log-viewer/TargetBadge.svelte | 2 +- .../src/admin/log-viewer/UserBadge.svelte | 2 +- .../src/admin/player/PlayerToolbar.svelte | 2 +- .../src/common/ConnectionInstructions.svelte | 8 +- .../common/CredentialUsedStateBadge.svelte | 5 +- warpgate-web/src/common/Loadable.svelte | 2 +- warpgate-web/src/common/NavListItem.svelte | 2 +- warpgate-web/src/common/RadioButton.svelte | 2 +- warpgate-web/src/common/ThemeSwitcher.svelte | 3 +- warpgate-web/src/common/helpers.ts | 34 +++ .../common/sveltestrap-s5-ports/Alert.svelte | 67 ------ .../common/sveltestrap-s5-ports/Badge.svelte | 72 ------- .../sveltestrap-s5-ports/ModalHeader.svelte | 36 ---- .../sveltestrap-s5-ports/Tooltip.svelte | 203 ------------------ .../sveltestrap-s5-ports/_sveltestrapUtils.ts | 34 --- .../src/gateway/ApiTokenManager.svelte | 4 +- .../src/gateway/CreateApiTokenModal.svelte | 2 +- .../src/gateway/CredentialManager.svelte | 4 +- warpgate-web/src/gateway/Login.svelte | 3 +- warpgate-web/src/gateway/OutOfBandAuth.svelte | 2 +- .../src/gateway/ProfileCredentials.svelte | 2 +- .../src/gateway/TicketRequests.svelte | 3 +- 71 files changed, 332 insertions(+), 643 deletions(-) delete mode 100644 warpgate-web/src/common/sveltestrap-s5-ports/Alert.svelte delete mode 100644 warpgate-web/src/common/sveltestrap-s5-ports/Badge.svelte delete mode 100644 warpgate-web/src/common/sveltestrap-s5-ports/ModalHeader.svelte delete mode 100644 warpgate-web/src/common/sveltestrap-s5-ports/Tooltip.svelte delete mode 100644 warpgate-web/src/common/sveltestrap-s5-ports/_sveltestrapUtils.ts diff --git a/.github/workflows/test.yml b/.github/workflows/test.yml index f58d774de..497f29e18 100644 --- a/.github/workflows/test.yml +++ b/.github/workflows/test.yml @@ -39,9 +39,6 @@ jobs: cargo clean rustup component add llvm-tools-preview - - name: Run Rust unit tests - run: cargo llvm-cov test --workspace - - name: Build UI run: | just npm ci @@ -54,6 +51,9 @@ jobs: - name: Build RDP helper run: just build-rdp-helper + - name: Run Rust unit tests + run: cargo llvm-cov test --workspace + - name: Build images working-directory: tests run: | diff --git a/warpgate-core/src/auth_state_store.rs b/warpgate-core/src/auth_state_store.rs index 8b3bcf470..e261df831 100644 --- a/warpgate-core/src/auth_state_store.rs +++ b/warpgate-core/src/auth_state_store.rs @@ -5,10 +5,10 @@ use std::time::{Duration, Instant}; use tokio::sync::{Mutex, broadcast}; use uuid::Uuid; -use warpgate_common::auth::{AuthResult, AuthState, CredentialKind}; +use warpgate_common::auth::{AuthResult, AuthState, CredentialKind, CredentialPolicy}; use warpgate_common::helpers::ipnet::WarpgateIpNet; use warpgate_common::helpers::username::username_eq_ci; -use warpgate_common::{SessionId, WarpgateError}; +use warpgate_common::{SessionId, User, WarpgateError}; use crate::login_protection::{FailedAttemptInfo, LoginProtectionService}; use crate::{ConfigProvider, ConfigProviderEnum}; @@ -61,6 +61,32 @@ fn check_ip_allowed( )) } +/// Record a failed attempt for an unknown username so that username +/// enumeration counts toward IP blocking, just like a wrong password would. +/// +/// `credential_type` is `None` for contexts that must not be penalised — +/// notably SSH public-key offers, which legitimately fail as clients try +/// each agent key in turn — in which case nothing is recorded. +async fn record_unknown_user_attempt( + login_protection: &LoginProtectionService, + username: &str, + protocol: &str, + remote_ip: Option, + credential_type: Option<&str>, +) { + let (Some(remote_ip), Some(credential_type)) = (remote_ip, credential_type) else { + return; + }; + let _ = login_protection + .record_failed_attempt(FailedAttemptInfo { + username: username.to_string(), + remote_ip, + protocol: protocol.to_string(), + credential_type: credential_type.to_string(), + }) + .await; +} + struct AuthCompletionSignal { sender: broadcast::Sender, created_at: Instant, @@ -73,79 +99,38 @@ impl AuthCompletionSignal { } pub struct AuthStateStore { - config_provider: Arc>, - login_protection: Arc, store: HashMap>, Instant)>, completion_signals: HashMap, web_auth_request_signal: broadcast::Sender, } +impl Default for AuthStateStore { + fn default() -> Self { + Self::new() + } +} + impl AuthStateStore { - pub fn new( - config_provider: Arc>, - login_protection: Arc, - ) -> Self { + pub fn new() -> Self { Self { store: HashMap::new(), - config_provider, - login_protection, completion_signals: HashMap::new(), web_auth_request_signal: broadcast::channel(100).0, } } - /// Record a failed attempt for an unknown username so that username - /// enumeration counts toward IP blocking, just like a wrong password would. - /// - /// `credential_type` is `None` for contexts that must not be penalised — - /// notably SSH public-key offers, which legitimately fail as clients try - /// each agent key in turn — in which case nothing is recorded. - async fn record_unknown_user_attempt( - &self, - username: &str, - protocol: &str, - remote_ip: Option, - credential_type: Option<&str>, - ) { - let (Some(remote_ip), Some(credential_type)) = (remote_ip, credential_type) else { - return; - }; - let _ = self - .login_protection - .record_failed_attempt(FailedAttemptInfo { - username: username.to_string(), - remote_ip, - protocol: protocol.to_string(), - credential_type: credential_type.to_string(), - }) - .await; - } - pub fn contains_key(&self, id: &Uuid) -> bool { self.store.contains_key(id) } - pub async fn all_pending_web_auths_for_user( - &self, - username: &str, - ) -> Vec>> { - let mut results = vec![]; - for auth in self.store.values() { - { - let inner = auth.0.lock().await; - if !username_eq_ci(&inner.user_info().username, username) { - continue; - } - let AuthResult::Need(need) = inner.verify() else { - continue; - }; - if !need.contains(&CredentialKind::WebUserApproval) { - continue; - } - } - results.push(auth.0.clone()); - } - results + /// Returns cloned `Arc` handles to every stored [`AuthState`]. + /// + /// This only clones the handles and never locks the inner states, so the + /// store lock is held for the shortest possible time. Callers can then + /// inspect each state (which requires locking it) *after* releasing the + /// store lock, avoiding lock convoys on the store under concurrent logins. + pub fn snapshot_states(&self) -> Vec>> { + self.store.values().map(|auth| auth.0.clone()).collect() } pub fn get(&self, id: &Uuid) -> Option>> { @@ -156,19 +141,24 @@ impl AuthStateStore { self.web_auth_request_signal.subscribe() } - pub async fn create( - &mut self, - session_id: Option<&SessionId>, + /// Resolves the user record and credential policy for an authentication + /// attempt. + /// + /// This performs the config-provider database lookups (`list_users`, + /// `get_credential_policy`) and the IP-range check **without** holding the + /// [`AuthStateStore`] lock. Callers must run this before locking the store + /// and pass the result to [`AuthStateStore::create`], so that concurrent + /// logins don't serialise on the store lock while doing database I/O. + pub(crate) async fn resolve_user_and_policy( + config_provider: &Arc>, + login_protection: &LoginProtectionService, username: &str, protocol: &str, supported_credential_types: &[CredentialKind], remote_ip: Option, rate_limit_credential_type: Option<&str>, - ) -> Result<(Uuid, Arc>), WarpgateError> { - let id = Uuid::new_v4(); - - let Some(user) = self - .config_provider + ) -> Result<(User, Box), WarpgateError> { + let Some(user) = config_provider .lock() .await .list_users() @@ -177,7 +167,8 @@ impl AuthStateStore { .find(|u| username_eq_ci(&u.username, username)) .cloned() else { - self.record_unknown_user_attempt( + record_unknown_user_attempt( + login_protection, username, protocol, remote_ip, @@ -189,14 +180,14 @@ impl AuthStateStore { check_ip_allowed(user.allowed_ip_ranges.as_ref(), remote_ip, username)?; - let policy = self - .config_provider + let policy = config_provider .lock() .await .get_credential_policy(username, supported_credential_types) .await?; let Some(policy) = policy else { - self.record_unknown_user_attempt( + record_unknown_user_attempt( + login_protection, username, protocol, remote_ip, @@ -206,6 +197,24 @@ impl AuthStateStore { return Err(WarpgateError::UserNotFound(username.into())); }; + Ok((user, policy)) + } + + /// Creates and stores a new [`AuthState`] from an already-resolved user and + /// credential policy (see [`AuthStateStore::resolve_user_and_policy`]). + /// + /// This is deliberately synchronous and does no database I/O, so the store + /// lock is only held for the in-memory insert. + pub(crate) fn create( + &mut self, + session_id: Option<&SessionId>, + user: &User, + protocol: &str, + policy: Box, + remote_ip: Option, + ) -> (Uuid, Arc>) { + let id = Uuid::new_v4(); + let (state_change_tx, mut state_change_rx) = broadcast::channel(1); let web_auth_request_signal = self.web_auth_request_signal.clone(); tokio::spawn(async move { @@ -220,16 +229,15 @@ impl AuthStateStore { id, session_id.copied(), remote_ip, - (&user).into(), + user.into(), protocol.to_string(), policy, state_change_tx, ); - self.store - .insert(id, (Arc::new(Mutex::new(state)), Instant::now())); + let state_arc = Arc::new(Mutex::new(state)); + self.store.insert(id, (state_arc.clone(), Instant::now())); - #[allow(clippy::unwrap_used)] - Ok((id, self.get(&id).unwrap())) + (id, state_arc) } pub fn subscribe(&mut self, id: Uuid) -> broadcast::Receiver { diff --git a/warpgate-core/src/services.rs b/warpgate-core/src/services.rs index 07bf80d2e..a0576f723 100644 --- a/warpgate-core/src/services.rs +++ b/warpgate-core/src/services.rs @@ -1,3 +1,4 @@ +use std::net::IpAddr; use std::sync::Arc; use std::time::Duration; @@ -5,7 +6,9 @@ use anyhow::Result; use sea_orm::DatabaseConnection; use tokio::sync::Mutex; use tracing::warn; -use warpgate_common::{GlobalParams, WarpgateConfig}; +use uuid::Uuid; +use warpgate_common::auth::{AuthState, CredentialKind}; +use warpgate_common::{GlobalParams, SessionId, WarpgateConfig, WarpgateError}; use crate::db::{connect_to_db_and_migrate, populate_db}; use crate::login_protection::LoginProtectionService; @@ -46,10 +49,7 @@ impl Services { let login_protection = Arc::new(LoginProtectionService::new(db.clone()).await?); - let auth_state_store = Arc::new(Mutex::new(AuthStateStore::new( - config_provider.clone(), - login_protection.clone(), - ))); + let auth_state_store = Arc::new(Mutex::new(AuthStateStore::new())); tokio::spawn({ let auth_state_store = auth_state_store.clone(); @@ -98,4 +98,34 @@ impl Services { global_params: Arc::new(params), }) } + + /// Resolves the user/policy (without the store lock) and inserts a new + /// [`AuthState`] under a brief store lock. This is the only sanctioned way + /// to create an auth state, so the "no DB I/O while holding the store lock" + /// invariant is enforced structurally rather than by convention. + pub async fn create_auth_state( + &self, + session_id: Option<&SessionId>, + username: &str, + protocol: &str, + supported_credential_types: &[CredentialKind], + remote_ip: Option, + rate_limit_credential_type: Option<&str>, + ) -> Result<(Uuid, Arc>), WarpgateError> { + let (user, policy) = AuthStateStore::resolve_user_and_policy( + &self.config_provider, + &self.login_protection, + username, + protocol, + supported_credential_types, + remote_ip, + rate_limit_credential_type, + ) + .await?; + Ok(self + .auth_state_store + .lock() + .await + .create(session_id, &user, protocol, policy, remote_ip)) + } } diff --git a/warpgate-desktop-auth/src/lib.rs b/warpgate-desktop-auth/src/lib.rs index 3b1f13948..7a8ce6517 100644 --- a/warpgate-desktop-auth/src/lib.rs +++ b/warpgate-desktop-auth/src/lib.rs @@ -108,12 +108,10 @@ pub async fn authenticate( return Ok(DesktopAuthOutcome::Failed); } + let session_id = server_handle.lock().await.id(); let (state_id, state_arc) = services - .auth_state_store - .lock() - .await - .create( - Some(&server_handle.lock().await.id()), + .create_auth_state( + Some(&session_id), &username, P::NAME, &[ diff --git a/warpgate-protocol-http/src/api/auth.rs b/warpgate-protocol-http/src/api/auth.rs index f9c2da311..118254c40 100644 --- a/warpgate-protocol-http/src/api/auth.rs +++ b/warpgate-protocol-http/src/api/auth.rs @@ -11,7 +11,7 @@ use poem_openapi::param::Path; use poem_openapi::payload::Json; use poem_openapi::{ApiResponse, Enum, Object, OpenApi}; use time::OffsetDateTime; -use tokio::sync::Mutex; +use tokio::sync::{Mutex, broadcast}; use tracing::{error, warn}; use uuid::Uuid; use warpgate_admin::api::AnySecurityScheme; @@ -436,8 +436,11 @@ impl Api { let Some(state_id) = session.get_auth_state_id() else { return Ok(AuthStateResponse::NotFound); }; - let store = services.auth_state_store.lock().await; - let Some(state_arc) = store.get(&state_id.0) else { + let state_arc = { + let store = services.auth_state_store.lock().await; + store.get(&state_id.0) + }; + let Some(state_arc) = state_arc else { return Ok(AuthStateResponse::NotFound); }; serialize_auth_state_inner(state_arc, services) @@ -460,12 +463,20 @@ impl Api { let Some(state_id) = session.get_auth_state_id() else { return Ok(AuthStateResponse::NotFound); }; - let mut store = services.auth_state_store.lock().await; - let Some(state_arc) = store.get(&state_id.0) else { + let state_arc = { + let store = services.auth_state_store.lock().await; + store.get(&state_id.0) + }; + let Some(state_arc) = state_arc else { return Ok(AuthStateResponse::NotFound); }; state_arc.lock().await.reject(); - store.complete(&state_id.0).await; + services + .auth_state_store + .lock() + .await + .complete(&state_id.0) + .await; session.clear_auth_state(); serialize_auth_state_inner(state_arc, services) @@ -486,19 +497,36 @@ impl Api { _sec_scheme: AnySecurityScheme, ) -> poem::Result { let services = ctx.services(); - let store = services.auth_state_store.lock().await; let RequestAuthorization::Session(SessionAuthorization::User { username, .. }) = &ctx.auth else { return Ok(AuthStateListResponse::NotFound); }; - let state_arcs = store.all_pending_web_auths_for_user(username).await; + // Snapshot the state handles while briefly holding the store lock, then + // release it before inspecting/serialising each state. Inspecting a + // state locks its inner mutex (and `serialize_auth_state_inner` locks + // the session state store), so doing that work under the auth state + // store lock would serialise every login against this endpoint. + let state_arcs = { + let store = services.auth_state_store.lock().await; + store.snapshot_states() + }; let mut results = vec![]; for state_arc in state_arcs { - results.push(serialize_auth_state_inner(state_arc, services).await?); + let is_pending_web_approval = { + let state = state_arc.lock().await; + username_eq_ci(&state.user_info().username, username) + && matches!( + state.verify(), + AuthResult::Need(need) if need.contains(&CredentialKind::WebUserApproval) + ) + }; + if is_pending_web_approval { + results.push(serialize_auth_state_inner(state_arc, services).await?); + } } Ok(AuthStateListResponse::Ok(Json(results))) @@ -595,14 +623,15 @@ async fn get_foreign_auth_state( id: &Uuid, ctx: &AuthenticatedRequestContext, ) -> Option>> { - let store = ctx.services().auth_state_store.lock().await; - let RequestAuthorization::Session(SessionAuthorization::User { username, .. }) = &ctx.auth else { return None; }; - let state_arc = store.get(id)?; + let state_arc = { + let store = ctx.services().auth_state_store.lock().await; + store.get(id)? + }; { let state = state_arc.lock().await; @@ -620,10 +649,15 @@ async fn serialize_auth_state_inner( ) -> poem::Result { let state = state_arc.lock().await; - let session_state_store = services.state.lock().await; - let session_state = state - .session_id() - .and_then(|session_id| session_state_store.sessions.get(session_id)); + // Clone the session state handle under a brief session-store lock, then + // release it before locking the per-session mutex, so we never hold the + // session state store lock across another lock acquisition. + let session_state = { + let session_state_store = services.state.lock().await; + state + .session_id() + .and_then(|session_id| session_state_store.sessions.get(session_id).cloned()) + }; let peer_addr = match session_state { Some(x) => x.lock().await.remote_address, @@ -663,13 +697,30 @@ pub async fn api_get_web_auth_requests_stream( Ok(ws.on_upgrade(|socket| async move { let (mut sink, _) = socket.split(); - while let Ok(id) = rx.recv().await { - let auth_state_store = auth_state_store.lock().await; - if let Some(state) = auth_state_store.get(&id) { - let state = state.lock().await; - if username_eq_ci(&state.user_info().username, &username) { - sink.send(Message::Text(id.to_string())).await?; - } + loop { + let id = match rx.recv().await { + Ok(id) => id, + // The signal channel only carries wake-ups; if we lag behind we + // can safely resync on the next event instead of tearing down. + Err(broadcast::error::RecvError::Lagged(_)) => continue, + Err(broadcast::error::RecvError::Closed) => break, + }; + + // Clone the state handle under a brief store lock, then release it + // before locking the inner state, so we never hold the store lock + // across an inner-state lock (which protocol sessions hold across + // DB I/O) or the socket write. + let state_arc = { + let store = auth_state_store.lock().await; + store.get(&id) + }; + let belongs_to_user = match state_arc { + Some(state) => username_eq_ci(&state.lock().await.user_info().username, &username), + None => false, + }; + + if belongs_to_user { + sink.send(Message::Text(id.to_string())).await?; } } diff --git a/warpgate-protocol-http/src/common.rs b/warpgate-protocol-http/src/common.rs index b9b5d5f40..7985f8800 100644 --- a/warpgate-protocol-http/src/common.rs +++ b/warpgate-protocol-http/src/common.rs @@ -210,9 +210,9 @@ pub async fn get_or_create_auth_state_for_request( } } - let mut store = ctx.services().auth_state_store.lock().await; - let (id, state) = store - .create( + let (id, state) = ctx + .services() + .create_auth_state( None, username, crate::common::PROTOCOL_NAME, diff --git a/warpgate-protocol-mysql/src/session.rs b/warpgate-protocol-mysql/src/session.rs index 0d9186aba..ea51bfded 100644 --- a/warpgate-protocol-mysql/src/session.rs +++ b/warpgate-protocol-mysql/src/session.rs @@ -240,13 +240,11 @@ impl MySqlSession { } } + let session_id = self.server_handle.lock().await.id(); let state_arc = self .services - .auth_state_store - .lock() - .await - .create( - Some(&self.server_handle.lock().await.id()), + .create_auth_state( + Some(&session_id), &username, crate::common::PROTOCOL_NAME, &[CredentialKind::Password], diff --git a/warpgate-protocol-postgres/src/session.rs b/warpgate-protocol-postgres/src/session.rs index 6948adcf7..52499f650 100644 --- a/warpgate-protocol-postgres/src/session.rs +++ b/warpgate-protocol-postgres/src/session.rs @@ -227,13 +227,11 @@ impl PostgresSession { } } + let session_id = self.server_handle.lock().await.id(); let state_arc = self .services - .auth_state_store - .lock() - .await - .create( - Some(&self.server_handle.lock().await.id()), + .create_auth_state( + Some(&session_id), &username, crate::common::PROTOCOL_NAME, &[CredentialKind::Password], diff --git a/warpgate-protocol-ssh/src/server/session.rs b/warpgate-protocol-ssh/src/server/session.rs index 38523a345..c660ca993 100644 --- a/warpgate-protocol-ssh/src/server/session.rs +++ b/warpgate-protocol-ssh/src/server/session.rs @@ -298,8 +298,8 @@ impl ServerSession { kinds } - /// `rate_limit_credential_type` is forwarded to `AuthStateStore::create` so - /// an unknown username is recorded as a failed attempt for IP blocking — + /// `rate_limit_credential_type` is forwarded to `Services::create_auth_state` + /// so an unknown username is recorded as a failed attempt for IP blocking — /// `None` for benign contexts (public-key offers) that must not be counted. async fn get_auth_state( &mut self, @@ -322,10 +322,7 @@ impl ServerSession { { let state = self .services - .auth_state_store - .lock() - .await - .create( + .create_auth_state( Some(&self.id), username, crate::PROTOCOL_NAME, diff --git a/warpgate-web/package-lock.json b/warpgate-web/package-lock.json index fc8f0cb31..ab4afe32e 100644 --- a/warpgate-web/package-lock.json +++ b/warpgate-web/package-lock.json @@ -24,7 +24,7 @@ "@otplib/preset-browser": "^12.0.1", "@stylistic/eslint-plugin": "^5.10.0", "@sveltejs/vite-plugin-svelte": "^6.2.4", - "@sveltestrap/sveltestrap": "^6.2.7", + "@sveltestrap/sveltestrap": "^7", "@tsconfig/svelte": "^5.0.8", "@types/qrcode": "^1.5.6", "@types/ua-parser-js": "^0.7.36", @@ -1060,6 +1060,7 @@ "integrity": "sha512-9zHxaDDM+oXW9As6UsP5yYB+UqczBmpeSCIFWdPEtEukMnZhxODG1BBjaUcdBB8Sc1uzojSJSJlp3yFp853t1g==", "dev": true, "license": "MIT", + "peer": true, "dependencies": { "file-type": "21.3.4", "iterare": "1.2.1", @@ -1274,6 +1275,7 @@ "integrity": "sha512-P1st0aksCrn9sGZhp8GMYwBnQsbvAWsZAX44oXNNvLHGqAOcoVxmjZiohstwQ7SqKnbR47akdNi+uleWD8+g6A==", "dev": true, "license": "MIT", + "peer": true, "funding": { "type": "opencollective", "url": "https://opencollective.com/popperjs" @@ -1682,6 +1684,7 @@ "integrity": "sha512-ou/d51QSdTyN26D7h6dSpusAKaZkAiGM55/AKYi+9AGZw7q85hElbjK3kEyzXHhLSnRISHOYzVge6x0jRZ7DXA==", "dev": true, "license": "MIT", + "peer": true, "dependencies": { "@sveltejs/vite-plugin-svelte-inspector": "^5.0.0", "deepmerge": "^4.3.1", @@ -1726,9 +1729,9 @@ } }, "node_modules/@sveltestrap/sveltestrap": { - "version": "6.2.8", - "resolved": "https://registry.npmjs.org/@sveltestrap/sveltestrap/-/sveltestrap-6.2.8.tgz", - "integrity": "sha512-uOcY3xsHtJ/VE0Z8A5JrJIq4kW/OOQcct5JtJW5ug646Re2pzQH08P7RjSp04UUjmR/RlC3ugVfiGss8idHG7A==", + "version": "7.1.0", + "resolved": "https://registry.npmjs.org/@sveltestrap/sveltestrap/-/sveltestrap-7.1.0.tgz", + "integrity": "sha512-TpIx25kqLV+z+VD3yfqYayOI1IaCeWFbT0uqM6NfA4vQgDs9PjFwmjkU4YEAlV/ngs9e7xPmaRWE7lkrg4Miow==", "dev": true, "license": "MIT", "dependencies": { @@ -1940,6 +1943,7 @@ "integrity": "sha512-5B7PfA2e1NQGCnDHd/0lW7W3gvp3d59Ryw54FYO8Uswxo9f6ikw3AZV+Xj/TvpImmpsiYyUqAfhC6kJID1jF6w==", "dev": true, "license": "MIT", + "peer": true, "dependencies": { "@typescript-eslint/scope-manager": "8.61.0", "@typescript-eslint/types": "8.61.0", @@ -2514,6 +2518,7 @@ "resolved": "https://registry.npmjs.org/acorn/-/acorn-8.15.0.tgz", "integrity": "sha512-NZyJarBfL7nWwIq+FDL6Zp/yHEhePMNnnJ0y3qfieCrmNvYct8uvtiV41UvlSe6apAfk0fY1FbWx+NwfmpvtTg==", "license": "MIT", + "peer": true, "bin": { "acorn": "bin/acorn" }, @@ -2803,6 +2808,7 @@ "integrity": "sha512-J8SwNxprqqpbfenehxWYXE7CW+wM1BB4w3+N+g+/Wx40xM4rsLrfPmHHxSWIxJLYDgSY/HqlFPIYb2/S3rxafw==", "dev": true, "license": "MIT", + "peer": true, "dependencies": { "follow-redirects": "^1.16.0", "form-data": "^4.0.5", @@ -3813,6 +3819,7 @@ "integrity": "sha512-LEyamqS7W5HB3ujJyvi0HQK/dtVINZvd5mAAp9eT5S/ujByGjiZLCzPcHVzuXbpJDJF/cxwHlfceVUDZ2lnSTw==", "dev": true, "license": "MIT", + "peer": true, "dependencies": { "@eslint-community/eslint-utils": "^4.8.0", "@eslint-community/regexpp": "^4.12.1", @@ -4002,6 +4009,7 @@ "integrity": "sha512-whOE1HFo/qJDyX4SnXzP4N6zOWn79WhnCUY/iDR0mPfQZO8wcYE4JClzI2oZrhBnnMUCBCHZhO6VQyoBU95mZA==", "dev": true, "license": "MIT", + "peer": true, "dependencies": { "@rtsao/scc": "^1.1.0", "array-includes": "^3.1.9", @@ -6312,6 +6320,7 @@ } ], "license": "MIT", + "peer": true, "dependencies": { "nanoid": "^3.3.11", "picocolors": "^1.1.1", @@ -6648,7 +6657,8 @@ "resolved": "https://registry.npmjs.org/quickjs-wasi/-/quickjs-wasi-2.2.0.tgz", "integrity": "sha512-zQxXmQMrEoD3S+jQdYsloq4qAuaxKFHZj6hHqOYGwB2iQZH+q9e/lf5zQPXCKOk0WJuAjzRFbO4KwHIp2D05Iw==", "dev": true, - "license": "MIT" + "license": "MIT", + "peer": true }, "node_modules/readdirp": { "version": "3.6.0", @@ -6681,7 +6691,8 @@ "resolved": "https://registry.npmjs.org/reflect-metadata/-/reflect-metadata-0.2.2.tgz", "integrity": "sha512-urBwgfrvVP/eAyXx4hluJivBKzuEbSQs9rKWCrCkbSxNv8mxPcUZKeuoF3Uy4mJl3Lwprp6yy5/39VWigZ4K6Q==", "dev": true, - "license": "Apache-2.0" + "license": "Apache-2.0", + "peer": true }, "node_modules/reflect.getprototypeof": { "version": "1.0.10", @@ -6869,6 +6880,7 @@ "integrity": "sha512-dhKf903U/PQZY6boNNtAGdWbG85WAbjT/1xYoZIC7FAY0yWapOBQVsVrDl58W86//e1VpMNBtRV4MaXfdMySFA==", "dev": true, "license": "Apache-2.0", + "peer": true, "dependencies": { "tslib": "^2.1.0" } @@ -6947,6 +6959,7 @@ "integrity": "sha512-AaIqGSrjo5lA2Yg7RvFZrlXDBCp3nV4XP73GrLGvdRWWwk+8H3l0SDvq/5bA4eF+0RFPLuWUk3E+P1U/YqnpsQ==", "dev": true, "license": "MIT", + "peer": true, "dependencies": { "chokidar": ">=3.0.0 <4.0.0", "immutable": "^4.0.0", @@ -7406,6 +7419,7 @@ "resolved": "https://registry.npmjs.org/svelte/-/svelte-5.56.3.tgz", "integrity": "sha512-w7JvrM5IFl5cmfbY0TLik9o7mjRUJmRMhOR51tBPu708Gr/MjbGs7VnJnr/B0CaXeI4vtnOh7RKxDr0cwhMdDA==", "license": "MIT", + "peer": true, "dependencies": { "@jridgewell/remapping": "^2.3.4", "@jridgewell/sourcemap-codec": "^1.5.0", @@ -7854,6 +7868,7 @@ "integrity": "sha512-jl1vZzPDinLr9eUt3J/t7V6FgNEw9QjvBPdysz9KfQDD41fQrC2Y4vKQdiaUpFT4bXlb1RHhLpp8wtm6M5TgSw==", "dev": true, "license": "Apache-2.0", + "peer": true, "bin": { "tsc": "bin/tsc", "tsserver": "bin/tsserver" @@ -8007,6 +8022,7 @@ "integrity": "sha512-EFrL7Hw4kmhZdwWO3dwwFJo6hO3FXuQ6Bg8BK/faHZ9m1YxqBS31BNSTxklIQkxK/4LlV8zTYnPsIRLBzTzjCA==", "dev": true, "hasInstallScript": true, + "peer": true, "dependencies": { "napi-postinstall": "^0.3.0" }, @@ -8058,6 +8074,7 @@ "integrity": "sha512-w+N7Hifpc3gRjZ63vYBXA56dvvRlNWRczTdmCBBa+CotUzAPf5b7YMdMR/8CQoeYE5LX3W4wj6RYTgonm1b9DA==", "dev": true, "license": "MIT", + "peer": true, "dependencies": { "esbuild": "^0.27.0", "fdir": "^6.5.0", diff --git a/warpgate-web/package.json b/warpgate-web/package.json index 14a30f431..7bb004289 100644 --- a/warpgate-web/package.json +++ b/warpgate-web/package.json @@ -31,7 +31,7 @@ "@otplib/preset-browser": "^12.0.1", "@stylistic/eslint-plugin": "^5.10.0", "@sveltejs/vite-plugin-svelte": "^6.2.4", - "@sveltestrap/sveltestrap": "^6.2.7", + "@sveltestrap/sveltestrap": "^7", "@tsconfig/svelte": "^5.0.8", "@types/qrcode": "^1.5.6", "@types/ua-parser-js": "^0.7.36", diff --git a/warpgate-web/src/admin/AnalyticsConsentModal.svelte b/warpgate-web/src/admin/AnalyticsConsentModal.svelte index 04c592e4d..32d3ad0db 100644 --- a/warpgate-web/src/admin/AnalyticsConsentModal.svelte +++ b/warpgate-web/src/admin/AnalyticsConsentModal.svelte @@ -3,8 +3,7 @@ import { reloadServerInfo } from 'gateway/lib/store' import AsyncButton from 'common/AsyncButton.svelte' import AnalyticsPreview from './AnalyticsPreview.svelte' - import { Modal, ModalBody, ModalFooter } from '@sveltestrap/sveltestrap' - import ModalHeader from 'common/sveltestrap-s5-ports/ModalHeader.svelte' + import { Modal, ModalBody, ModalFooter, ModalHeader } from '@sveltestrap/sveltestrap' import HelpText from './lib/HelpText.svelte' interface Props { diff --git a/warpgate-web/src/admin/AnalyticsPreview.svelte b/warpgate-web/src/admin/AnalyticsPreview.svelte index 244a3a7de..2e02cd162 100644 --- a/warpgate-web/src/admin/AnalyticsPreview.svelte +++ b/warpgate-web/src/admin/AnalyticsPreview.svelte @@ -1,7 +1,7 @@ - -{#if isOpen} - -{/if} diff --git a/warpgate-web/src/common/sveltestrap-s5-ports/Badge.svelte b/warpgate-web/src/common/sveltestrap-s5-ports/Badge.svelte deleted file mode 100644 index e93c37cd7..000000000 --- a/warpgate-web/src/common/sveltestrap-s5-ports/Badge.svelte +++ /dev/null @@ -1,72 +0,0 @@ - - -{#if href} - - {@render children?.()} - {#if positioned || indicator} - {ariaLabel} - {/if} - -{:else} - - {@render children?.()} - {#if positioned || indicator} - {ariaLabel} - {/if} - -{/if} diff --git a/warpgate-web/src/common/sveltestrap-s5-ports/ModalHeader.svelte b/warpgate-web/src/common/sveltestrap-s5-ports/ModalHeader.svelte deleted file mode 100644 index cd4f960fc..000000000 --- a/warpgate-web/src/common/sveltestrap-s5-ports/ModalHeader.svelte +++ /dev/null @@ -1,36 +0,0 @@ - - -
- - {#if close}{@render close()}{:else} - {#if typeof toggle === 'function'} - - {/if} - {/if} -
diff --git a/warpgate-web/src/common/sveltestrap-s5-ports/Tooltip.svelte b/warpgate-web/src/common/sveltestrap-s5-ports/Tooltip.svelte deleted file mode 100644 index dc1ea58bc..000000000 --- a/warpgate-web/src/common/sveltestrap-s5-ports/Tooltip.svelte +++ /dev/null @@ -1,203 +0,0 @@ - - - -{#if isOpen} -{@const SvelteComponent = outer} - - - -{/if} diff --git a/warpgate-web/src/common/sveltestrap-s5-ports/_sveltestrapUtils.ts b/warpgate-web/src/common/sveltestrap-s5-ports/_sveltestrapUtils.ts deleted file mode 100644 index 73f203c26..000000000 --- a/warpgate-web/src/common/sveltestrap-s5-ports/_sveltestrapUtils.ts +++ /dev/null @@ -1,34 +0,0 @@ -// eslint-disable-next-line @typescript-eslint/explicit-module-boundary-types -export function toClassName(value: any) { - let result = '' - - if (typeof value === 'string' || typeof value === 'number') { - result += value - } else if (typeof value === 'object') { - if (Array.isArray(value)) { - result = value.map(toClassName).filter(Boolean).join(' ') - } else { - for (const key in value) { - if (value[key]) { - // eslint-disable-next-line @typescript-eslint/no-unused-expressions - result && (result += ' ') - result += key - } - } - } - } - - return result -} - -// eslint-disable-next-line @typescript-eslint/explicit-module-boundary-types -export const classnames = (...args: any[]) => args.map(toClassName).filter(Boolean).join(' ') - - -export function uuid(): string { - return 'xxxxxxxx-xxxx-4xxx-yxxx-xxxxxxxxxxxx'.replace(/[xy]/g, (c) => { - const r = (Math.random() * 16) | 0 - const v = c === 'x' ? r : (r & 0x3) | 0x8 - return v.toString(16) - }) -} diff --git a/warpgate-web/src/gateway/ApiTokenManager.svelte b/warpgate-web/src/gateway/ApiTokenManager.svelte index add06674c..6efe503eb 100644 --- a/warpgate-web/src/gateway/ApiTokenManager.svelte +++ b/warpgate-web/src/gateway/ApiTokenManager.svelte @@ -5,9 +5,9 @@ import { faKey } from '@fortawesome/free-solid-svg-icons' import Fa from 'svelte-fa' import CreateApiTokenModal from './CreateApiTokenModal.svelte' - import Alert from 'common/sveltestrap-s5-ports/Alert.svelte' + import {Alert} from '@sveltestrap/sveltestrap' import CopyButton from 'common/CopyButton.svelte' - import Badge from 'common/sveltestrap-s5-ports/Badge.svelte' + import {Badge} from '@sveltestrap/sveltestrap' import EmptyState from 'common/EmptyState.svelte' import { Button } from '@sveltestrap/sveltestrap' import { querystring } from 'svelte-spa-router' diff --git a/warpgate-web/src/gateway/CreateApiTokenModal.svelte b/warpgate-web/src/gateway/CreateApiTokenModal.svelte index 634e0a4f2..08176c470 100644 --- a/warpgate-web/src/gateway/CreateApiTokenModal.svelte +++ b/warpgate-web/src/gateway/CreateApiTokenModal.svelte @@ -7,10 +7,10 @@ Modal, ModalBody, ModalFooter, + ModalHeader, } from '@sveltestrap/sveltestrap' import { serverInfo } from 'gateway/lib/store' - import ModalHeader from 'common/sveltestrap-s5-ports/ModalHeader.svelte' interface Props { isOpen: boolean diff --git a/warpgate-web/src/gateway/CredentialManager.svelte b/warpgate-web/src/gateway/CredentialManager.svelte index b52d64284..5bca12749 100644 --- a/warpgate-web/src/gateway/CredentialManager.svelte +++ b/warpgate-web/src/gateway/CredentialManager.svelte @@ -2,7 +2,6 @@ import { api, CredentialKind, PasswordState, type CredentialsState, type ExistingOtpCredential, type ExistingPublicKeyCredential, type ExistingCertificateCredential } from 'gateway/lib/api' import { serverInfo } from 'gateway/lib/store' import { deleteCertificateKey } from 'gateway/lib/certificateStore' - import Alert from 'common/sveltestrap-s5-ports/Alert.svelte' import { faCertificate, faIdBadge, faKey, faKeyboard, faMobilePhone } from '@fortawesome/free-solid-svg-icons' import Fa from 'svelte-fa' import PublicKeyCredentialModal from 'admin/PublicKeyCredentialModal.svelte' @@ -11,8 +10,7 @@ import CreateOtpModal from 'admin/CreateOtpModal.svelte' import CredentialUsedStateBadge from 'common/CredentialUsedStateBadge.svelte' import Loadable from 'common/Loadable.svelte' - import { Button } from '@sveltestrap/sveltestrap' - import Tooltip from 'common/sveltestrap-s5-ports/Tooltip.svelte' + import { Button, Alert, Tooltip } from '@sveltestrap/sveltestrap' let creds: CredentialsState | undefined = $state() diff --git a/warpgate-web/src/gateway/Login.svelte b/warpgate-web/src/gateway/Login.svelte index 64fa70909..9a8027856 100644 --- a/warpgate-web/src/gateway/Login.svelte +++ b/warpgate-web/src/gateway/Login.svelte @@ -1,7 +1,7 @@ diff --git a/warpgate-web/src/gateway/TicketRequests.svelte b/warpgate-web/src/gateway/TicketRequests.svelte index 83d9bbafb..d87971779 100644 --- a/warpgate-web/src/gateway/TicketRequests.svelte +++ b/warpgate-web/src/gateway/TicketRequests.svelte @@ -8,8 +8,7 @@ import Fa from 'svelte-fa' import { faTicket, faEyeSlash } from '@fortawesome/free-solid-svg-icons' import { stringifyError } from 'common/errors' - import Alert from 'common/sveltestrap-s5-ports/Alert.svelte' - import { FormGroup, Button, Modal, ModalBody, ModalFooter } from '@sveltestrap/sveltestrap' + import { FormGroup, Button, Modal, ModalBody, ModalFooter, Alert } from '@sveltestrap/sveltestrap' import EmptyState from 'common/EmptyState.svelte' import Loadable from 'common/Loadable.svelte' import { statusIcon, statusColor } from 'common/ticketRequestStatus' From 8ec44f9bed4ee8d4e83b478f55b6d1078a46a110 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Sun, 5 Jul 2026 22:27:53 +0200 Subject: [PATCH 258/556] Bump the version-bumps group across 1 directory with 3 updates (#2121) Signed-off-by: dependabot[bot] --- tests/poetry.lock | 20 ++++++++++---------- 1 file changed, 10 insertions(+), 10 deletions(-) diff --git a/tests/poetry.lock b/tests/poetry.lock index b95d424e8..0043a51fa 100644 --- a/tests/poetry.lock +++ b/tests/poetry.lock @@ -708,18 +708,18 @@ ssh = ["bcrypt (>=3.1.5)"] [[package]] name = "deepmerge" -version = "2.0" +version = "2.1.0" description = "A toolset for deeply merging Python dictionaries." optional = false python-versions = ">=3.8" groups = ["main"] files = [ - {file = "deepmerge-2.0-py3-none-any.whl", hash = "sha256:6de9ce507115cff0bed95ff0ce9ecc31088ef50cbdf09bc90a09349a318b3d00"}, - {file = "deepmerge-2.0.tar.gz", hash = "sha256:5c3d86081fbebd04dd5de03626a0607b809a98fb6ccba5770b62466fe940ff20"}, + {file = "deepmerge-2.1.0-py3-none-any.whl", hash = "sha256:8f148339a91d680a75ecb74ade235d9e759a93df373a0b04e9d31c8666cfeb75"}, + {file = "deepmerge-2.1.0.tar.gz", hash = "sha256:07ca7a7b8935df596c512fa8161877c0487ac61f691c07766e7d71d2b23bdd2f"}, ] [package.extras] -dev = ["black", "build", "mypy", "pytest", "pyupgrade", "twine", "validate-pyproject[all]"] +dev = ["black", "build", "mypy", "pytest", "pyupgrade", "sphinx", "sphinx-rtd-theme", "twine", "validate-pyproject[all]"] [[package]] name = "exceptiongroup" @@ -1842,14 +1842,14 @@ test = ["coverage", "mypy", "ruff", "wheel"] [[package]] name = "pyright" -version = "1.1.410" +version = "1.1.411" description = "Command line wrapper for pyright" optional = false python-versions = ">=3.7" groups = ["dev"] files = [ - {file = "pyright-1.1.410-py3-none-any.whl", hash = "sha256:5e961bed37cacf96b3f7cd7b1da39b350a9239aa2e69138d0e88f728cfaf296c"}, - {file = "pyright-1.1.410.tar.gz", hash = "sha256:07a073b8ba6749826773c1269773efa11b93440d9a6aa60419d9a3172d6dc488"}, + {file = "pyright-1.1.411-py3-none-any.whl", hash = "sha256:dc7c72a8e2700c55baa127554040e067041ea53ccfd50bf96308cc4291c7d5d9"}, + {file = "pyright-1.1.411.tar.gz", hash = "sha256:d885a0551f2e763b089a02702174e7f4ba77548cddabc972ab86d1f7f1b0f998"}, ] [package.dependencies] @@ -1863,14 +1863,14 @@ nodejs = ["nodejs-wheel-binaries"] [[package]] name = "pytest" -version = "9.1.0" +version = "9.1.1" description = "pytest: simple powerful testing with Python" optional = false python-versions = ">=3.10" groups = ["main", "dev"] files = [ - {file = "pytest-9.1.0-py3-none-any.whl", hash = "sha256:8ebb0e7888bdf2bdfc602ec51f8f62d50200af37356c74e503c79a94f5c81f32"}, - {file = "pytest-9.1.0.tar.gz", hash = "sha256:41dd9148c08072446394cefd3d79701701335a9f4cae69ba92e39f6c7f5c061c"}, + {file = "pytest-9.1.1-py3-none-any.whl", hash = "sha256:37a86b45efb9a47a61a36449063e8e18d0cab3161329fc099eb21783169c4f0c"}, + {file = "pytest-9.1.1.tar.gz", hash = "sha256:1088fbde8f2b49d95a549a195707afa7a76a3ce9bcadc26b6d71f0ffda5fe313"}, ] [package.dependencies] From b015c23deff4d332c37a173c71d29d75ba8df5d1 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Sun, 5 Jul 2026 22:28:02 +0200 Subject: [PATCH 259/556] Bump docker/setup-qemu-action from 4.1.0 to 4.2.0 (#2133) Signed-off-by: dependabot[bot] --- .github/workflows/docker.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/docker.yml b/.github/workflows/docker.yml index 7564d7f9b..e82a88bea 100644 --- a/.github/workflows/docker.yml +++ b/.github/workflows/docker.yml @@ -40,7 +40,7 @@ jobs: fetch-depth: 0 - name: Set up QEMU - uses: docker/setup-qemu-action@06116385d9baf250c9f4dcb4858b16962ea869c3 + uses: docker/setup-qemu-action@96fe6ef7f33517b61c61be40b68a1882f3264fb8 - name: Set up Docker Buildx uses: docker/setup-buildx-action@d7f5e7f509e45cec5c76c4d5afdd7de93d0b3df5 From a3ef6500c7cc5a0037415e3394e0d48efea69943 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Sun, 5 Jul 2026 23:33:27 +0200 Subject: [PATCH 260/556] Bump vite and @sveltejs/vite-plugin-svelte in /warpgate-web (#2084) Signed-off-by: dependabot[bot] --- warpgate-web/package-lock.json | 1342 +++++++++++++------------------- warpgate-web/package.json | 4 +- 2 files changed, 531 insertions(+), 815 deletions(-) diff --git a/warpgate-web/package-lock.json b/warpgate-web/package-lock.json index ab4afe32e..55ec94acd 100644 --- a/warpgate-web/package-lock.json +++ b/warpgate-web/package-lock.json @@ -23,7 +23,7 @@ "@otplib/plugin-crypto-js": "^12.0.1", "@otplib/preset-browser": "^12.0.1", "@stylistic/eslint-plugin": "^5.10.0", - "@sveltejs/vite-plugin-svelte": "^6.2.4", + "@sveltejs/vite-plugin-svelte": "^7.1.2", "@sveltestrap/sveltestrap": "^7", "@tsconfig/svelte": "^5.0.8", "@types/qrcode": "^1.5.6", @@ -58,7 +58,7 @@ "typescript": "^5.9.3", "typescript-eslint": "^8.61.0", "ua-parser-js": "^2.0.10", - "vite": "^7.3.1", + "vite": "^8.1.3", "vite-tsconfig-paths": "^6.1.1", "zmodem.js": "^0.1.10" } @@ -75,21 +75,21 @@ } }, "node_modules/@emnapi/core": { - "version": "1.7.1", - "resolved": "https://registry.npmjs.org/@emnapi/core/-/core-1.7.1.tgz", - "integrity": "sha512-o1uhUASyo921r2XtHYOHy7gdkGLge8ghBEQHMWmyJFoXlpU58kIrhhN3w26lpQb6dspetweapMn2CSNwQ8I4wg==", + "version": "1.11.1", + "resolved": "https://registry.npmjs.org/@emnapi/core/-/core-1.11.1.tgz", + "integrity": "sha512-RSvbQmHzdKzNsLYa/wHrbc3KN4sYLKAdPZxqiM2HATqv/SBk2/ENSHpvXGaLOMcsAyz0poEGqkmmKYG3OWiJEQ==", "dev": true, "license": "MIT", "optional": true, "dependencies": { - "@emnapi/wasi-threads": "1.1.0", + "@emnapi/wasi-threads": "1.2.2", "tslib": "^2.4.0" } }, "node_modules/@emnapi/runtime": { - "version": "1.7.1", - "resolved": "https://registry.npmjs.org/@emnapi/runtime/-/runtime-1.7.1.tgz", - "integrity": "sha512-PVtJr5CmLwYAU9PZDMITZoR5iAOShYREoR45EyyLrbntV50mdePTgUn4AmOw90Ifcj+x2kRjdzr1HP3RrNiHGA==", + "version": "1.11.1", + "resolved": "https://registry.npmjs.org/@emnapi/runtime/-/runtime-1.11.1.tgz", + "integrity": "sha512-vgj7R3y3Wgx24IQaGPA/R6YFXLHVMOZ0uVEyIQPaWs+rd1AzfEMXlAC22FYwO1XkKR6NPsq7mUandH8oIRdZFw==", "dev": true, "license": "MIT", "optional": true, @@ -98,9 +98,9 @@ } }, "node_modules/@emnapi/wasi-threads": { - "version": "1.1.0", - "resolved": "https://registry.npmjs.org/@emnapi/wasi-threads/-/wasi-threads-1.1.0.tgz", - "integrity": "sha512-WI0DdZ8xFSbgMjR1sFsKABJ/C5OnRrjT06JXbZKexJGrDuPTzZdDYfFlsgcCXCyf+suG5QU2e/y1Wo2V/OapLQ==", + "version": "1.2.2", + "resolved": "https://registry.npmjs.org/@emnapi/wasi-threads/-/wasi-threads-1.2.2.tgz", + "integrity": "sha512-c95qOXkHdydNKhscBTebqEC1CVAZpyqOfVfBzQ1qgzyl3gfeldUjIggDbIZgDKsHLgnsM+igH7TJ/eAasaVuMA==", "dev": true, "license": "MIT", "optional": true, @@ -108,448 +108,6 @@ "tslib": "^2.4.0" } }, - "node_modules/@esbuild/aix-ppc64": { - "version": "0.27.2", - "resolved": "https://registry.npmjs.org/@esbuild/aix-ppc64/-/aix-ppc64-0.27.2.tgz", - "integrity": "sha512-GZMB+a0mOMZs4MpDbj8RJp4cw+w1WV5NYD6xzgvzUJ5Ek2jerwfO2eADyI6ExDSUED+1X8aMbegahsJi+8mgpw==", - "cpu": [ - "ppc64" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "aix" - ], - "engines": { - "node": ">=18" - } - }, - "node_modules/@esbuild/android-arm": { - "version": "0.27.2", - "resolved": "https://registry.npmjs.org/@esbuild/android-arm/-/android-arm-0.27.2.tgz", - "integrity": "sha512-DVNI8jlPa7Ujbr1yjU2PfUSRtAUZPG9I1RwW4F4xFB1Imiu2on0ADiI/c3td+KmDtVKNbi+nffGDQMfcIMkwIA==", - "cpu": [ - "arm" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "android" - ], - "engines": { - "node": ">=18" - } - }, - "node_modules/@esbuild/android-arm64": { - "version": "0.27.2", - "resolved": "https://registry.npmjs.org/@esbuild/android-arm64/-/android-arm64-0.27.2.tgz", - "integrity": "sha512-pvz8ZZ7ot/RBphf8fv60ljmaoydPU12VuXHImtAs0XhLLw+EXBi2BLe3OYSBslR4rryHvweW5gmkKFwTiFy6KA==", - "cpu": [ - "arm64" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "android" - ], - "engines": { - "node": ">=18" - } - }, - "node_modules/@esbuild/android-x64": { - "version": "0.27.2", - "resolved": "https://registry.npmjs.org/@esbuild/android-x64/-/android-x64-0.27.2.tgz", - "integrity": "sha512-z8Ank4Byh4TJJOh4wpz8g2vDy75zFL0TlZlkUkEwYXuPSgX8yzep596n6mT7905kA9uHZsf/o2OJZubl2l3M7A==", - "cpu": [ - "x64" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "android" - ], - "engines": { - "node": ">=18" - } - }, - "node_modules/@esbuild/darwin-arm64": { - "version": "0.27.2", - "resolved": "https://registry.npmjs.org/@esbuild/darwin-arm64/-/darwin-arm64-0.27.2.tgz", - "integrity": "sha512-davCD2Zc80nzDVRwXTcQP/28fiJbcOwvdolL0sOiOsbwBa72kegmVU0Wrh1MYrbuCL98Omp5dVhQFWRKR2ZAlg==", - "cpu": [ - "arm64" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "darwin" - ], - "engines": { - "node": ">=18" - } - }, - "node_modules/@esbuild/darwin-x64": { - "version": "0.27.2", - "resolved": "https://registry.npmjs.org/@esbuild/darwin-x64/-/darwin-x64-0.27.2.tgz", - "integrity": "sha512-ZxtijOmlQCBWGwbVmwOF/UCzuGIbUkqB1faQRf5akQmxRJ1ujusWsb3CVfk/9iZKr2L5SMU5wPBi1UWbvL+VQA==", - "cpu": [ - "x64" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "darwin" - ], - "engines": { - "node": ">=18" - } - }, - "node_modules/@esbuild/freebsd-arm64": { - "version": "0.27.2", - "resolved": "https://registry.npmjs.org/@esbuild/freebsd-arm64/-/freebsd-arm64-0.27.2.tgz", - "integrity": "sha512-lS/9CN+rgqQ9czogxlMcBMGd+l8Q3Nj1MFQwBZJyoEKI50XGxwuzznYdwcav6lpOGv5BqaZXqvBSiB/kJ5op+g==", - "cpu": [ - "arm64" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "freebsd" - ], - "engines": { - "node": ">=18" - } - }, - "node_modules/@esbuild/freebsd-x64": { - "version": "0.27.2", - "resolved": "https://registry.npmjs.org/@esbuild/freebsd-x64/-/freebsd-x64-0.27.2.tgz", - "integrity": "sha512-tAfqtNYb4YgPnJlEFu4c212HYjQWSO/w/h/lQaBK7RbwGIkBOuNKQI9tqWzx7Wtp7bTPaGC6MJvWI608P3wXYA==", - "cpu": [ - "x64" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "freebsd" - ], - "engines": { - "node": ">=18" - } - }, - "node_modules/@esbuild/linux-arm": { - "version": "0.27.2", - "resolved": "https://registry.npmjs.org/@esbuild/linux-arm/-/linux-arm-0.27.2.tgz", - "integrity": "sha512-vWfq4GaIMP9AIe4yj1ZUW18RDhx6EPQKjwe7n8BbIecFtCQG4CfHGaHuh7fdfq+y3LIA2vGS/o9ZBGVxIDi9hw==", - "cpu": [ - "arm" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "linux" - ], - "engines": { - "node": ">=18" - } - }, - "node_modules/@esbuild/linux-arm64": { - "version": "0.27.2", - "resolved": "https://registry.npmjs.org/@esbuild/linux-arm64/-/linux-arm64-0.27.2.tgz", - "integrity": "sha512-hYxN8pr66NsCCiRFkHUAsxylNOcAQaxSSkHMMjcpx0si13t1LHFphxJZUiGwojB1a/Hd5OiPIqDdXONia6bhTw==", - "cpu": [ - "arm64" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "linux" - ], - "engines": { - "node": ">=18" - } - }, - "node_modules/@esbuild/linux-ia32": { - "version": "0.27.2", - "resolved": "https://registry.npmjs.org/@esbuild/linux-ia32/-/linux-ia32-0.27.2.tgz", - "integrity": "sha512-MJt5BRRSScPDwG2hLelYhAAKh9imjHK5+NE/tvnRLbIqUWa+0E9N4WNMjmp/kXXPHZGqPLxggwVhz7QP8CTR8w==", - "cpu": [ - "ia32" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "linux" - ], - "engines": { - "node": ">=18" - } - }, - "node_modules/@esbuild/linux-loong64": { - "version": "0.27.2", - "resolved": "https://registry.npmjs.org/@esbuild/linux-loong64/-/linux-loong64-0.27.2.tgz", - "integrity": "sha512-lugyF1atnAT463aO6KPshVCJK5NgRnU4yb3FUumyVz+cGvZbontBgzeGFO1nF+dPueHD367a2ZXe1NtUkAjOtg==", - "cpu": [ - "loong64" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "linux" - ], - "engines": { - "node": ">=18" - } - }, - "node_modules/@esbuild/linux-mips64el": { - "version": "0.27.2", - "resolved": "https://registry.npmjs.org/@esbuild/linux-mips64el/-/linux-mips64el-0.27.2.tgz", - "integrity": "sha512-nlP2I6ArEBewvJ2gjrrkESEZkB5mIoaTswuqNFRv/WYd+ATtUpe9Y09RnJvgvdag7he0OWgEZWhviS1OTOKixw==", - "cpu": [ - "mips64el" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "linux" - ], - "engines": { - "node": ">=18" - } - }, - "node_modules/@esbuild/linux-ppc64": { - "version": "0.27.2", - "resolved": "https://registry.npmjs.org/@esbuild/linux-ppc64/-/linux-ppc64-0.27.2.tgz", - "integrity": "sha512-C92gnpey7tUQONqg1n6dKVbx3vphKtTHJaNG2Ok9lGwbZil6DrfyecMsp9CrmXGQJmZ7iiVXvvZH6Ml5hL6XdQ==", - "cpu": [ - "ppc64" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "linux" - ], - "engines": { - "node": ">=18" - } - }, - "node_modules/@esbuild/linux-riscv64": { - "version": "0.27.2", - "resolved": "https://registry.npmjs.org/@esbuild/linux-riscv64/-/linux-riscv64-0.27.2.tgz", - "integrity": "sha512-B5BOmojNtUyN8AXlK0QJyvjEZkWwy/FKvakkTDCziX95AowLZKR6aCDhG7LeF7uMCXEJqwa8Bejz5LTPYm8AvA==", - "cpu": [ - "riscv64" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "linux" - ], - "engines": { - "node": ">=18" - } - }, - "node_modules/@esbuild/linux-s390x": { - "version": "0.27.2", - "resolved": "https://registry.npmjs.org/@esbuild/linux-s390x/-/linux-s390x-0.27.2.tgz", - "integrity": "sha512-p4bm9+wsPwup5Z8f4EpfN63qNagQ47Ua2znaqGH6bqLlmJ4bx97Y9JdqxgGZ6Y8xVTixUnEkoKSHcpRlDnNr5w==", - "cpu": [ - "s390x" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "linux" - ], - "engines": { - "node": ">=18" - } - }, - "node_modules/@esbuild/linux-x64": { - "version": "0.27.2", - "resolved": "https://registry.npmjs.org/@esbuild/linux-x64/-/linux-x64-0.27.2.tgz", - "integrity": "sha512-uwp2Tip5aPmH+NRUwTcfLb+W32WXjpFejTIOWZFw/v7/KnpCDKG66u4DLcurQpiYTiYwQ9B7KOeMJvLCu/OvbA==", - "cpu": [ - "x64" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "linux" - ], - "engines": { - "node": ">=18" - } - }, - "node_modules/@esbuild/netbsd-arm64": { - "version": "0.27.2", - "resolved": "https://registry.npmjs.org/@esbuild/netbsd-arm64/-/netbsd-arm64-0.27.2.tgz", - "integrity": "sha512-Kj6DiBlwXrPsCRDeRvGAUb/LNrBASrfqAIok+xB0LxK8CHqxZ037viF13ugfsIpePH93mX7xfJp97cyDuTZ3cw==", - "cpu": [ - "arm64" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "netbsd" - ], - "engines": { - "node": ">=18" - } - }, - "node_modules/@esbuild/netbsd-x64": { - "version": "0.27.2", - "resolved": "https://registry.npmjs.org/@esbuild/netbsd-x64/-/netbsd-x64-0.27.2.tgz", - "integrity": "sha512-HwGDZ0VLVBY3Y+Nw0JexZy9o/nUAWq9MlV7cahpaXKW6TOzfVno3y3/M8Ga8u8Yr7GldLOov27xiCnqRZf0tCA==", - "cpu": [ - "x64" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "netbsd" - ], - "engines": { - "node": ">=18" - } - }, - "node_modules/@esbuild/openbsd-arm64": { - "version": "0.27.2", - "resolved": "https://registry.npmjs.org/@esbuild/openbsd-arm64/-/openbsd-arm64-0.27.2.tgz", - "integrity": "sha512-DNIHH2BPQ5551A7oSHD0CKbwIA/Ox7+78/AWkbS5QoRzaqlev2uFayfSxq68EkonB+IKjiuxBFoV8ESJy8bOHA==", - "cpu": [ - "arm64" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "openbsd" - ], - "engines": { - "node": ">=18" - } - }, - "node_modules/@esbuild/openbsd-x64": { - "version": "0.27.2", - "resolved": "https://registry.npmjs.org/@esbuild/openbsd-x64/-/openbsd-x64-0.27.2.tgz", - "integrity": "sha512-/it7w9Nb7+0KFIzjalNJVR5bOzA9Vay+yIPLVHfIQYG/j+j9VTH84aNB8ExGKPU4AzfaEvN9/V4HV+F+vo8OEg==", - "cpu": [ - "x64" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "openbsd" - ], - "engines": { - "node": ">=18" - } - }, - "node_modules/@esbuild/openharmony-arm64": { - "version": "0.27.2", - "resolved": "https://registry.npmjs.org/@esbuild/openharmony-arm64/-/openharmony-arm64-0.27.2.tgz", - "integrity": "sha512-LRBbCmiU51IXfeXk59csuX/aSaToeG7w48nMwA6049Y4J4+VbWALAuXcs+qcD04rHDuSCSRKdmY63sruDS5qag==", - "cpu": [ - "arm64" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "openharmony" - ], - "engines": { - "node": ">=18" - } - }, - "node_modules/@esbuild/sunos-x64": { - "version": "0.27.2", - "resolved": "https://registry.npmjs.org/@esbuild/sunos-x64/-/sunos-x64-0.27.2.tgz", - "integrity": "sha512-kMtx1yqJHTmqaqHPAzKCAkDaKsffmXkPHThSfRwZGyuqyIeBvf08KSsYXl+abf5HDAPMJIPnbBfXvP2ZC2TfHg==", - "cpu": [ - "x64" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "sunos" - ], - "engines": { - "node": ">=18" - } - }, - "node_modules/@esbuild/win32-arm64": { - "version": "0.27.2", - "resolved": "https://registry.npmjs.org/@esbuild/win32-arm64/-/win32-arm64-0.27.2.tgz", - "integrity": "sha512-Yaf78O/B3Kkh+nKABUF++bvJv5Ijoy9AN1ww904rOXZFLWVc5OLOfL56W+C8F9xn5JQZa3UX6m+IktJnIb1Jjg==", - "cpu": [ - "arm64" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "win32" - ], - "engines": { - "node": ">=18" - } - }, - "node_modules/@esbuild/win32-ia32": { - "version": "0.27.2", - "resolved": "https://registry.npmjs.org/@esbuild/win32-ia32/-/win32-ia32-0.27.2.tgz", - "integrity": "sha512-Iuws0kxo4yusk7sw70Xa2E2imZU5HoixzxfGCdxwBdhiDgt9vX9VUCBhqcwY7/uh//78A1hMkkROMJq9l27oLQ==", - "cpu": [ - "ia32" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "win32" - ], - "engines": { - "node": ">=18" - } - }, - "node_modules/@esbuild/win32-x64": { - "version": "0.27.2", - "resolved": "https://registry.npmjs.org/@esbuild/win32-x64/-/win32-x64-0.27.2.tgz", - "integrity": "sha512-sRdU18mcKf7F+YgheI/zGf5alZatMUTKj/jNS6l744f9u3WFu4v7twcUI9vu4mknF4Y9aDlblIie0IM+5xxaqQ==", - "cpu": [ - "x64" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "win32" - ], - "engines": { - "node": ">=18" - } - }, "node_modules/@eslint-community/eslint-utils": { "version": "4.9.1", "resolved": "https://registry.npmjs.org/@eslint-community/eslint-utils/-/eslint-utils-4.9.1.tgz", @@ -1060,7 +618,6 @@ "integrity": "sha512-9zHxaDDM+oXW9As6UsP5yYB+UqczBmpeSCIFWdPEtEukMnZhxODG1BBjaUcdBB8Sc1uzojSJSJlp3yFp853t1g==", "dev": true, "license": "MIT", - "peer": true, "dependencies": { "file-type": "21.3.4", "iterare": "1.2.1", @@ -1269,36 +826,31 @@ "dev": true, "license": "MIT" }, + "node_modules/@oxc-project/types": { + "version": "0.138.0", + "resolved": "https://registry.npmjs.org/@oxc-project/types/-/types-0.138.0.tgz", + "integrity": "sha512-1a7ZKmrRTCoN1XMZ4L0PyyqrMnrNlLyPuOkdSX2MZg7IiIGRUyurNhAm73ptDOraoBcIordsIGKNPKUzy3ZmfA==", + "dev": true, + "license": "MIT", + "funding": { + "url": "https://github.com/sponsors/Boshen" + } + }, "node_modules/@popperjs/core": { "version": "2.11.8", "resolved": "https://registry.npmjs.org/@popperjs/core/-/core-2.11.8.tgz", "integrity": "sha512-P1st0aksCrn9sGZhp8GMYwBnQsbvAWsZAX44oXNNvLHGqAOcoVxmjZiohstwQ7SqKnbR47akdNi+uleWD8+g6A==", "dev": true, "license": "MIT", - "peer": true, "funding": { "type": "opencollective", "url": "https://opencollective.com/popperjs" } }, - "node_modules/@rollup/rollup-android-arm-eabi": { - "version": "4.59.0", - "resolved": "https://registry.npmjs.org/@rollup/rollup-android-arm-eabi/-/rollup-android-arm-eabi-4.59.0.tgz", - "integrity": "sha512-upnNBkA6ZH2VKGcBj9Fyl9IGNPULcjXRlg0LLeaioQWueH30p6IXtJEbKAgvyv+mJaMxSm1l6xwDXYjpEMiLMg==", - "cpu": [ - "arm" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "android" - ] - }, - "node_modules/@rollup/rollup-android-arm64": { - "version": "4.59.0", - "resolved": "https://registry.npmjs.org/@rollup/rollup-android-arm64/-/rollup-android-arm64-4.59.0.tgz", - "integrity": "sha512-hZ+Zxj3SySm4A/DylsDKZAeVg0mvi++0PYVceVyX7hemkw7OreKdCvW2oQ3T1FMZvCaQXqOTHb8qmBShoqk69Q==", + "node_modules/@rolldown/binding-android-arm64": { + "version": "1.1.4", + "resolved": "https://registry.npmjs.org/@rolldown/binding-android-arm64/-/binding-android-arm64-1.1.4.tgz", + "integrity": "sha512-EZLpf/8y7GXkkra90ML47kzik/GMP3EMcE9bPyHmRfxLC6z9+aW5A8poCsoxjrT5GfEcNAAvWwUHjvP1pUQkfw==", "cpu": [ "arm64" ], @@ -1307,12 +859,15 @@ "optional": true, "os": [ "android" - ] + ], + "engines": { + "node": "^20.19.0 || >=22.12.0" + } }, - "node_modules/@rollup/rollup-darwin-arm64": { - "version": "4.59.0", - "resolved": "https://registry.npmjs.org/@rollup/rollup-darwin-arm64/-/rollup-darwin-arm64-4.59.0.tgz", - "integrity": "sha512-W2Psnbh1J8ZJw0xKAd8zdNgF9HRLkdWwwdWqubSVk0pUuQkoHnv7rx4GiF9rT4t5DIZGAsConRE3AxCdJ4m8rg==", + "node_modules/@rolldown/binding-darwin-arm64": { + "version": "1.1.4", + "resolved": "https://registry.npmjs.org/@rolldown/binding-darwin-arm64/-/binding-darwin-arm64-1.1.4.tgz", + "integrity": "sha512-aUi+HBvmYb7j8krl1+qJgkG8C17fO79gk3c+jPw4S8glRFc1DTija9S3EyaTSQUm5GJXYKDAsugBEhFHH2vYiQ==", "cpu": [ "arm64" ], @@ -1321,12 +876,15 @@ "optional": true, "os": [ "darwin" - ] + ], + "engines": { + "node": "^20.19.0 || >=22.12.0" + } }, - "node_modules/@rollup/rollup-darwin-x64": { - "version": "4.59.0", - "resolved": "https://registry.npmjs.org/@rollup/rollup-darwin-x64/-/rollup-darwin-x64-4.59.0.tgz", - "integrity": "sha512-ZW2KkwlS4lwTv7ZVsYDiARfFCnSGhzYPdiOU4IM2fDbL+QGlyAbjgSFuqNRbSthybLbIJ915UtZBtmuLrQAT/w==", + "node_modules/@rolldown/binding-darwin-x64": { + "version": "1.1.4", + "resolved": "https://registry.npmjs.org/@rolldown/binding-darwin-x64/-/binding-darwin-x64-1.1.4.tgz", + "integrity": "sha512-F7hHC3gwY11+vByKPRWqwGbeXWVgKmL+pTGCinaEhdihzBV2aQ0fvZOch9cXYUOKuKKq429HeYXOqQLc7wFCEg==", "cpu": [ "x64" ], @@ -1335,26 +893,15 @@ "optional": true, "os": [ "darwin" - ] - }, - "node_modules/@rollup/rollup-freebsd-arm64": { - "version": "4.59.0", - "resolved": "https://registry.npmjs.org/@rollup/rollup-freebsd-arm64/-/rollup-freebsd-arm64-4.59.0.tgz", - "integrity": "sha512-EsKaJ5ytAu9jI3lonzn3BgG8iRBjV4LxZexygcQbpiU0wU0ATxhNVEpXKfUa0pS05gTcSDMKpn3Sx+QB9RlTTA==", - "cpu": [ - "arm64" ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "freebsd" - ] + "engines": { + "node": "^20.19.0 || >=22.12.0" + } }, - "node_modules/@rollup/rollup-freebsd-x64": { - "version": "4.59.0", - "resolved": "https://registry.npmjs.org/@rollup/rollup-freebsd-x64/-/rollup-freebsd-x64-4.59.0.tgz", - "integrity": "sha512-d3DuZi2KzTMjImrxoHIAODUZYoUUMsuUiY4SRRcJy6NJoZ6iIqWnJu9IScV9jXysyGMVuW+KNzZvBLOcpdl3Vg==", + "node_modules/@rolldown/binding-freebsd-x64": { + "version": "1.1.4", + "resolved": "https://registry.npmjs.org/@rolldown/binding-freebsd-x64/-/binding-freebsd-x64-1.1.4.tgz", + "integrity": "sha512-sI5yw+7s92SK6odiEhD5lKCBlWcpjHS5qyqpVQbZAJ0fIzEUXrmbl3DH2ybR3PZogulNJF+COLtmA8hUfvkCCQ==", "cpu": [ "x64" ], @@ -1363,26 +910,15 @@ "optional": true, "os": [ "freebsd" - ] - }, - "node_modules/@rollup/rollup-linux-arm-gnueabihf": { - "version": "4.59.0", - "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm-gnueabihf/-/rollup-linux-arm-gnueabihf-4.59.0.tgz", - "integrity": "sha512-t4ONHboXi/3E0rT6OZl1pKbl2Vgxf9vJfWgmUoCEVQVxhW6Cw/c8I6hbbu7DAvgp82RKiH7TpLwxnJeKv2pbsw==", - "cpu": [ - "arm" ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "linux" - ] + "engines": { + "node": "^20.19.0 || >=22.12.0" + } }, - "node_modules/@rollup/rollup-linux-arm-musleabihf": { - "version": "4.59.0", - "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm-musleabihf/-/rollup-linux-arm-musleabihf-4.59.0.tgz", - "integrity": "sha512-CikFT7aYPA2ufMD086cVORBYGHffBo4K8MQ4uPS/ZnY54GKj36i196u8U+aDVT2LX4eSMbyHtyOh7D7Zvk2VvA==", + "node_modules/@rolldown/binding-linux-arm-gnueabihf": { + "version": "1.1.4", + "resolved": "https://registry.npmjs.org/@rolldown/binding-linux-arm-gnueabihf/-/binding-linux-arm-gnueabihf-1.1.4.tgz", + "integrity": "sha512-mCi0OKgEieFircrtVYmQAFGszRtMnZ6fpZAXrxanXAu7lqZcsK1E1RAaZNG0uKAnxox3B1f4EyQNnoyMfN1vAA==", "cpu": [ "arm" ], @@ -1391,180 +927,135 @@ "optional": true, "os": [ "linux" - ] - }, - "node_modules/@rollup/rollup-linux-arm64-gnu": { - "version": "4.59.0", - "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm64-gnu/-/rollup-linux-arm64-gnu-4.59.0.tgz", - "integrity": "sha512-jYgUGk5aLd1nUb1CtQ8E+t5JhLc9x5WdBKew9ZgAXg7DBk0ZHErLHdXM24rfX+bKrFe+Xp5YuJo54I5HFjGDAA==", - "cpu": [ - "arm64" ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "linux" - ] + "engines": { + "node": "^20.19.0 || >=22.12.0" + } }, - "node_modules/@rollup/rollup-linux-arm64-musl": { - "version": "4.59.0", - "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm64-musl/-/rollup-linux-arm64-musl-4.59.0.tgz", - "integrity": "sha512-peZRVEdnFWZ5Bh2KeumKG9ty7aCXzzEsHShOZEFiCQlDEepP1dpUl/SrUNXNg13UmZl+gzVDPsiCwnV1uI0RUA==", + "node_modules/@rolldown/binding-linux-arm64-gnu": { + "version": "1.1.4", + "resolved": "https://registry.npmjs.org/@rolldown/binding-linux-arm64-gnu/-/binding-linux-arm64-gnu-1.1.4.tgz", + "integrity": "sha512-B9Ial3Kv5sh0SHnB1g/QWcUQCEvCF6QKGAl4zXypYj65mVI+B4AhFBwPtSN7pDrJeIx8Z7zdy4ntx+wQABom7w==", "cpu": [ "arm64" ], "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "linux" - ] - }, - "node_modules/@rollup/rollup-linux-loong64-gnu": { - "version": "4.59.0", - "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-loong64-gnu/-/rollup-linux-loong64-gnu-4.59.0.tgz", - "integrity": "sha512-gbUSW/97f7+r4gHy3Jlup8zDG190AuodsWnNiXErp9mT90iCy9NKKU0Xwx5k8VlRAIV2uU9CsMnEFg/xXaOfXg==", - "cpu": [ - "loong64" + "libc": [ + "glibc" ], - "dev": true, "license": "MIT", "optional": true, "os": [ "linux" - ] - }, - "node_modules/@rollup/rollup-linux-loong64-musl": { - "version": "4.59.0", - "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-loong64-musl/-/rollup-linux-loong64-musl-4.59.0.tgz", - "integrity": "sha512-yTRONe79E+o0FWFijasoTjtzG9EBedFXJMl888NBEDCDV9I2wGbFFfJQQe63OijbFCUZqxpHz1GzpbtSFikJ4Q==", - "cpu": [ - "loong64" ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "linux" - ] + "engines": { + "node": "^20.19.0 || >=22.12.0" + } }, - "node_modules/@rollup/rollup-linux-ppc64-gnu": { - "version": "4.59.0", - "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-ppc64-gnu/-/rollup-linux-ppc64-gnu-4.59.0.tgz", - "integrity": "sha512-sw1o3tfyk12k3OEpRddF68a1unZ5VCN7zoTNtSn2KndUE+ea3m3ROOKRCZxEpmT9nsGnogpFP9x6mnLTCaoLkA==", + "node_modules/@rolldown/binding-linux-arm64-musl": { + "version": "1.1.4", + "resolved": "https://registry.npmjs.org/@rolldown/binding-linux-arm64-musl/-/binding-linux-arm64-musl-1.1.4.tgz", + "integrity": "sha512-lZVym0PuHE1KZ22gmFTC15lAkrg9iTszR617oYRB/iPY1A56ywoJzVKOJBKaot5RiikCObmur6pogpse3gRcng==", "cpu": [ - "ppc64" + "arm64" ], "dev": true, + "libc": [ + "musl" + ], "license": "MIT", "optional": true, "os": [ "linux" - ] + ], + "engines": { + "node": "^20.19.0 || >=22.12.0" + } }, - "node_modules/@rollup/rollup-linux-ppc64-musl": { - "version": "4.59.0", - "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-ppc64-musl/-/rollup-linux-ppc64-musl-4.59.0.tgz", - "integrity": "sha512-+2kLtQ4xT3AiIxkzFVFXfsmlZiG5FXYW7ZyIIvGA7Bdeuh9Z0aN4hVyXS/G1E9bTP/vqszNIN/pUKCk/BTHsKA==", + "node_modules/@rolldown/binding-linux-ppc64-gnu": { + "version": "1.1.4", + "resolved": "https://registry.npmjs.org/@rolldown/binding-linux-ppc64-gnu/-/binding-linux-ppc64-gnu-1.1.4.tgz", + "integrity": "sha512-t2DNiLJWNTbnEHyUzTumldML6ET4/g16467LZoDDJ3tSxGvguL5/NyC2lCsNKuyRycg9XeDQF5SSv+TNOhQEXg==", "cpu": [ "ppc64" ], "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "linux" - ] - }, - "node_modules/@rollup/rollup-linux-riscv64-gnu": { - "version": "4.59.0", - "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-riscv64-gnu/-/rollup-linux-riscv64-gnu-4.59.0.tgz", - "integrity": "sha512-NDYMpsXYJJaj+I7UdwIuHHNxXZ/b/N2hR15NyH3m2qAtb/hHPA4g4SuuvrdxetTdndfj9b1WOmy73kcPRoERUg==", - "cpu": [ - "riscv64" + "libc": [ + "glibc" ], - "dev": true, "license": "MIT", "optional": true, "os": [ "linux" - ] - }, - "node_modules/@rollup/rollup-linux-riscv64-musl": { - "version": "4.59.0", - "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-riscv64-musl/-/rollup-linux-riscv64-musl-4.59.0.tgz", - "integrity": "sha512-nLckB8WOqHIf1bhymk+oHxvM9D3tyPndZH8i8+35p/1YiVoVswPid2yLzgX7ZJP0KQvnkhM4H6QZ5m0LzbyIAg==", - "cpu": [ - "riscv64" ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "linux" - ] + "engines": { + "node": "^20.19.0 || >=22.12.0" + } }, - "node_modules/@rollup/rollup-linux-s390x-gnu": { - "version": "4.59.0", - "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-s390x-gnu/-/rollup-linux-s390x-gnu-4.59.0.tgz", - "integrity": "sha512-oF87Ie3uAIvORFBpwnCvUzdeYUqi2wY6jRFWJAy1qus/udHFYIkplYRW+wo+GRUP4sKzYdmE1Y3+rY5Gc4ZO+w==", + "node_modules/@rolldown/binding-linux-s390x-gnu": { + "version": "1.1.4", + "resolved": "https://registry.npmjs.org/@rolldown/binding-linux-s390x-gnu/-/binding-linux-s390x-gnu-1.1.4.tgz", + "integrity": "sha512-0WIRnL1Uw4BvTZRLQt+PVgo6ZKTJadlC2btP+/EOXv2f/DWbY0rEgl+y834mIVwP1FkTlWVTrGGJXf12lru7EQ==", "cpu": [ "s390x" ], "dev": true, + "libc": [ + "glibc" + ], "license": "MIT", "optional": true, "os": [ "linux" - ] + ], + "engines": { + "node": "^20.19.0 || >=22.12.0" + } }, - "node_modules/@rollup/rollup-linux-x64-gnu": { - "version": "4.59.0", - "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-x64-gnu/-/rollup-linux-x64-gnu-4.59.0.tgz", - "integrity": "sha512-3AHmtQq/ppNuUspKAlvA8HtLybkDflkMuLK4DPo77DfthRb71V84/c4MlWJXixZz4uruIH4uaa07IqoAkG64fg==", + "node_modules/@rolldown/binding-linux-x64-gnu": { + "version": "1.1.4", + "resolved": "https://registry.npmjs.org/@rolldown/binding-linux-x64-gnu/-/binding-linux-x64-gnu-1.1.4.tgz", + "integrity": "sha512-JWtGshGfX+oENAKonoNkqEJX+7hC8yfhi9GUyPX1VX4mdh1y5r+ZiJLR5XzAB0aoP6s/PcILsGjKq8O0mm24bw==", "cpu": [ "x64" ], "dev": true, + "libc": [ + "glibc" + ], "license": "MIT", "optional": true, "os": [ "linux" - ] + ], + "engines": { + "node": "^20.19.0 || >=22.12.0" + } }, - "node_modules/@rollup/rollup-linux-x64-musl": { - "version": "4.59.0", - "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-x64-musl/-/rollup-linux-x64-musl-4.59.0.tgz", - "integrity": "sha512-2UdiwS/9cTAx7qIUZB/fWtToJwvt0Vbo0zmnYt7ED35KPg13Q0ym1g442THLC7VyI6JfYTP4PiSOWyoMdV2/xg==", + "node_modules/@rolldown/binding-linux-x64-musl": { + "version": "1.1.4", + "resolved": "https://registry.npmjs.org/@rolldown/binding-linux-x64-musl/-/binding-linux-x64-musl-1.1.4.tgz", + "integrity": "sha512-rT6yQcxUuXs4CnbofqwHRRV0iem349rLMYpTjkgQGLjrY4ado/eDzwPZPTCgTOlF6Nkp8NEv70yLMTn6qkWxsQ==", "cpu": [ "x64" ], "dev": true, + "libc": [ + "musl" + ], "license": "MIT", "optional": true, "os": [ "linux" - ] - }, - "node_modules/@rollup/rollup-openbsd-x64": { - "version": "4.59.0", - "resolved": "https://registry.npmjs.org/@rollup/rollup-openbsd-x64/-/rollup-openbsd-x64-4.59.0.tgz", - "integrity": "sha512-M3bLRAVk6GOwFlPTIxVBSYKUaqfLrn8l0psKinkCFxl4lQvOSz8ZrKDz2gxcBwHFpci0B6rttydI4IpS4IS/jQ==", - "cpu": [ - "x64" ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "openbsd" - ] + "engines": { + "node": "^20.19.0 || >=22.12.0" + } }, - "node_modules/@rollup/rollup-openharmony-arm64": { - "version": "4.59.0", - "resolved": "https://registry.npmjs.org/@rollup/rollup-openharmony-arm64/-/rollup-openharmony-arm64-4.59.0.tgz", - "integrity": "sha512-tt9KBJqaqp5i5HUZzoafHZX8b5Q2Fe7UjYERADll83O4fGqJ49O1FsL6LpdzVFQcpwvnyd0i+K/VSwu/o/nWlA==", + "node_modules/@rolldown/binding-openharmony-arm64": { + "version": "1.1.4", + "resolved": "https://registry.npmjs.org/@rolldown/binding-openharmony-arm64/-/binding-openharmony-arm64-1.1.4.tgz", + "integrity": "sha512-KXMGoboq5cyaCQjDA4GLuRiOwBQ0EyFnJoVViLeZ45/3rFItRODEr+NdsBcVpll40hhNArlm/speWGRvj08LzA==", "cpu": [ "arm64" ], @@ -1573,54 +1064,70 @@ "optional": true, "os": [ "openharmony" - ] + ], + "engines": { + "node": "^20.19.0 || >=22.12.0" + } }, - "node_modules/@rollup/rollup-win32-arm64-msvc": { - "version": "4.59.0", - "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-arm64-msvc/-/rollup-win32-arm64-msvc-4.59.0.tgz", - "integrity": "sha512-V5B6mG7OrGTwnxaNUzZTDTjDS7F75PO1ae6MJYdiMu60sq0CqN5CVeVsbhPxalupvTX8gXVSU9gq+Rx1/hvu6A==", + "node_modules/@rolldown/binding-wasm32-wasi": { + "version": "1.1.4", + "resolved": "https://registry.npmjs.org/@rolldown/binding-wasm32-wasi/-/binding-wasm32-wasi-1.1.4.tgz", + "integrity": "sha512-5K83rb36oJiY7BCyE9zLZtGcPV4g5wvq+xwdO0XPIwDVZI8cyB/AUjkNXGb92/rnmezEkjMOpgY61rtwjQtFwg==", "cpu": [ - "arm64" + "wasm32" ], "dev": true, "license": "MIT", "optional": true, - "os": [ - "win32" - ] + "dependencies": { + "@emnapi/core": "1.11.1", + "@emnapi/runtime": "1.11.1", + "@napi-rs/wasm-runtime": "^1.1.6" + }, + "engines": { + "node": "^20.19.0 || >=22.12.0" + } }, - "node_modules/@rollup/rollup-win32-ia32-msvc": { - "version": "4.59.0", - "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-ia32-msvc/-/rollup-win32-ia32-msvc-4.59.0.tgz", - "integrity": "sha512-UKFMHPuM9R0iBegwzKF4y0C4J9u8C6MEJgFuXTBerMk7EJ92GFVFYBfOZaSGLu6COf7FxpQNqhNS4c4icUPqxA==", - "cpu": [ - "ia32" - ], + "node_modules/@rolldown/binding-wasm32-wasi/node_modules/@napi-rs/wasm-runtime": { + "version": "1.1.6", + "resolved": "https://registry.npmjs.org/@napi-rs/wasm-runtime/-/wasm-runtime-1.1.6.tgz", + "integrity": "sha512-ZLv/JdUfkvOy9eCnnBaGfiO+XimbjebAeO+MRQqD/B+FR1tnRN0tpKSJHRbE8sFfS6aqsXZ67TQjfwfsxULVbg==", "dev": true, "license": "MIT", "optional": true, - "os": [ - "win32" - ] + "dependencies": { + "@tybys/wasm-util": "^0.10.3" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/Brooooooklyn" + }, + "peerDependencies": { + "@emnapi/core": "^1.7.1", + "@emnapi/runtime": "^1.7.1" + } }, - "node_modules/@rollup/rollup-win32-x64-gnu": { - "version": "4.59.0", - "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-x64-gnu/-/rollup-win32-x64-gnu-4.59.0.tgz", - "integrity": "sha512-laBkYlSS1n2L8fSo1thDNGrCTQMmxjYY5G0WFWjFFYZkKPjsMBsgJfGf4TLxXrF6RyhI60L8TMOjBMvXiTcxeA==", + "node_modules/@rolldown/binding-win32-arm64-msvc": { + "version": "1.1.4", + "resolved": "https://registry.npmjs.org/@rolldown/binding-win32-arm64-msvc/-/binding-win32-arm64-msvc-1.1.4.tgz", + "integrity": "sha512-PnWBtw3TV5KOg69HQQDR0mnQuyCmSGR2pAB4DC1rPF808fgKeTUMj2EOEyKATpgiuxuR5APQmiDO7PDgEjTFSA==", "cpu": [ - "x64" + "arm64" ], "dev": true, "license": "MIT", "optional": true, "os": [ "win32" - ] + ], + "engines": { + "node": "^20.19.0 || >=22.12.0" + } }, - "node_modules/@rollup/rollup-win32-x64-msvc": { - "version": "4.59.0", - "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-x64-msvc/-/rollup-win32-x64-msvc-4.59.0.tgz", - "integrity": "sha512-2HRCml6OztYXyJXAvdDXPKcawukWY2GpR5/nxKp4iBgiO3wcoEGkAaqctIbZcNB6KlUQBIqt8VYkNSj2397EfA==", + "node_modules/@rolldown/binding-win32-x64-msvc": { + "version": "1.1.4", + "resolved": "https://registry.npmjs.org/@rolldown/binding-win32-x64-msvc/-/binding-win32-x64-msvc-1.1.4.tgz", + "integrity": "sha512-M1lpniBePobTfsa7Ks9a199e1akxsXn+GYBUKsEzv3YFzOm1HJAMNwKI3qr0Zq+mxwx9gOZoTdP1yXRYsZUocQ==", "cpu": [ "x64" ], @@ -1629,7 +1136,17 @@ "optional": true, "os": [ "win32" - ] + ], + "engines": { + "node": "^20.19.0 || >=22.12.0" + } + }, + "node_modules/@rolldown/pluginutils": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/@rolldown/pluginutils/-/pluginutils-1.0.1.tgz", + "integrity": "sha512-2j9bGt5Jh8hj+vPtgzPtl72j0yRxHAyumoo6TNfAjsLB04UtpSvPbPcDcBMxz7n+9CYB0c1GxQFxYRg2jimqGw==", + "dev": true, + "license": "MIT" }, "node_modules/@rtsao/scc": { "version": "1.1.0", @@ -1679,43 +1196,23 @@ } }, "node_modules/@sveltejs/vite-plugin-svelte": { - "version": "6.2.4", - "resolved": "https://registry.npmjs.org/@sveltejs/vite-plugin-svelte/-/vite-plugin-svelte-6.2.4.tgz", - "integrity": "sha512-ou/d51QSdTyN26D7h6dSpusAKaZkAiGM55/AKYi+9AGZw7q85hElbjK3kEyzXHhLSnRISHOYzVge6x0jRZ7DXA==", + "version": "7.1.2", + "resolved": "https://registry.npmjs.org/@sveltejs/vite-plugin-svelte/-/vite-plugin-svelte-7.1.2.tgz", + "integrity": "sha512-DrUBA2UXRfDmUX/ZTiEopd3X40yavsJF1FX2RygcuIScHL7o5YX1fMvoYnDhjeJQC4weCOklirpNWlcb2NiSeA==", "dev": true, "license": "MIT", - "peer": true, "dependencies": { - "@sveltejs/vite-plugin-svelte-inspector": "^5.0.0", "deepmerge": "^4.3.1", "magic-string": "^0.30.21", "obug": "^2.1.0", - "vitefu": "^1.1.1" - }, - "engines": { - "node": "^20.19 || ^22.12 || >=24" - }, - "peerDependencies": { - "svelte": "^5.0.0", - "vite": "^6.3.0 || ^7.0.0" - } - }, - "node_modules/@sveltejs/vite-plugin-svelte-inspector": { - "version": "5.0.1", - "resolved": "https://registry.npmjs.org/@sveltejs/vite-plugin-svelte-inspector/-/vite-plugin-svelte-inspector-5.0.1.tgz", - "integrity": "sha512-ubWshlMk4bc8mkwWbg6vNvCeT7lGQojE3ijDh3QTR6Zr/R+GXxsGbyH4PExEPpiFmqPhYiVSVmHBjUcVc1JIrA==", - "dev": true, - "license": "MIT", - "dependencies": { - "debug": "^4.4.1" + "vitefu": "^1.1.2" }, "engines": { "node": "^20.19 || ^22.12 || >=24" }, "peerDependencies": { - "@sveltejs/vite-plugin-svelte": "^6.0.0-next.0", - "svelte": "^5.0.0", - "vite": "^6.3.0 || ^7.0.0" + "svelte": "^5.46.4", + "vite": "^8.0.0-beta.7 || ^8.0.0" } }, "node_modules/@sveltejs/vite-plugin-svelte/node_modules/magic-string": { @@ -1818,9 +1315,9 @@ "license": "MIT" }, "node_modules/@tybys/wasm-util": { - "version": "0.10.1", - "resolved": "https://registry.npmjs.org/@tybys/wasm-util/-/wasm-util-0.10.1.tgz", - "integrity": "sha512-9tTaPJLSiejZKx+Bmog4uSubteqTvFrVrURwkmHixBo0G4seD0zUxp98E1DzUBJxLQ3NPwXrGKDiVjwx/DpPsg==", + "version": "0.10.3", + "resolved": "https://registry.npmjs.org/@tybys/wasm-util/-/wasm-util-0.10.3.tgz", + "integrity": "sha512-F3fo1MYrRJYL3zER0OUOmkutjr1Vp23m7OsSgp7nq4SP6OqX6C/56XFIPAl5bt3zaBRjmW7SGz3u/6LwFpYcOg==", "dev": true, "license": "MIT", "optional": true, @@ -1943,7 +1440,6 @@ "integrity": "sha512-5B7PfA2e1NQGCnDHd/0lW7W3gvp3d59Ryw54FYO8Uswxo9f6ikw3AZV+Xj/TvpImmpsiYyUqAfhC6kJID1jF6w==", "dev": true, "license": "MIT", - "peer": true, "dependencies": { "@typescript-eslint/scope-manager": "8.61.0", "@typescript-eslint/types": "8.61.0", @@ -2518,7 +2014,6 @@ "resolved": "https://registry.npmjs.org/acorn/-/acorn-8.15.0.tgz", "integrity": "sha512-NZyJarBfL7nWwIq+FDL6Zp/yHEhePMNnnJ0y3qfieCrmNvYct8uvtiV41UvlSe6apAfk0fY1FbWx+NwfmpvtTg==", "license": "MIT", - "peer": true, "bin": { "acorn": "bin/acorn" }, @@ -2808,7 +2303,6 @@ "integrity": "sha512-J8SwNxprqqpbfenehxWYXE7CW+wM1BB4w3+N+g+/Wx40xM4rsLrfPmHHxSWIxJLYDgSY/HqlFPIYb2/S3rxafw==", "dev": true, "license": "MIT", - "peer": true, "dependencies": { "follow-redirects": "^1.16.0", "form-data": "^4.0.5", @@ -3519,6 +3013,16 @@ ], "license": "MIT" }, + "node_modules/detect-libc": { + "version": "2.1.2", + "resolved": "https://registry.npmjs.org/detect-libc/-/detect-libc-2.1.2.tgz", + "integrity": "sha512-Btj2BOOO83o3WyH59e8MgXsxEQVcarkUOpEYrubB0urwnN10yQ364rsiByU11nZlqWYZm05i/of7io4mzihBtQ==", + "dev": true, + "license": "Apache-2.0", + "engines": { + "node": ">=8" + } + }, "node_modules/devalue": { "version": "5.8.1", "resolved": "https://registry.npmjs.org/devalue/-/devalue-5.8.1.tgz", @@ -3726,48 +3230,6 @@ "url": "https://github.com/sponsors/ljharb" } }, - "node_modules/esbuild": { - "version": "0.27.2", - "resolved": "https://registry.npmjs.org/esbuild/-/esbuild-0.27.2.tgz", - "integrity": "sha512-HyNQImnsOC7X9PMNaCIeAm4ISCQXs5a5YasTXVliKv4uuBo1dKrG0A+uQS8M5eXjVMnLg3WgXaKvprHlFJQffw==", - "dev": true, - "hasInstallScript": true, - "license": "MIT", - "bin": { - "esbuild": "bin/esbuild" - }, - "engines": { - "node": ">=18" - }, - "optionalDependencies": { - "@esbuild/aix-ppc64": "0.27.2", - "@esbuild/android-arm": "0.27.2", - "@esbuild/android-arm64": "0.27.2", - "@esbuild/android-x64": "0.27.2", - "@esbuild/darwin-arm64": "0.27.2", - "@esbuild/darwin-x64": "0.27.2", - "@esbuild/freebsd-arm64": "0.27.2", - "@esbuild/freebsd-x64": "0.27.2", - "@esbuild/linux-arm": "0.27.2", - "@esbuild/linux-arm64": "0.27.2", - "@esbuild/linux-ia32": "0.27.2", - "@esbuild/linux-loong64": "0.27.2", - "@esbuild/linux-mips64el": "0.27.2", - "@esbuild/linux-ppc64": "0.27.2", - "@esbuild/linux-riscv64": "0.27.2", - "@esbuild/linux-s390x": "0.27.2", - "@esbuild/linux-x64": "0.27.2", - "@esbuild/netbsd-arm64": "0.27.2", - "@esbuild/netbsd-x64": "0.27.2", - "@esbuild/openbsd-arm64": "0.27.2", - "@esbuild/openbsd-x64": "0.27.2", - "@esbuild/openharmony-arm64": "0.27.2", - "@esbuild/sunos-x64": "0.27.2", - "@esbuild/win32-arm64": "0.27.2", - "@esbuild/win32-ia32": "0.27.2", - "@esbuild/win32-x64": "0.27.2" - } - }, "node_modules/escalade": { "version": "3.2.0", "resolved": "https://registry.npmjs.org/escalade/-/escalade-3.2.0.tgz", @@ -3819,7 +3281,6 @@ "integrity": "sha512-LEyamqS7W5HB3ujJyvi0HQK/dtVINZvd5mAAp9eT5S/ujByGjiZLCzPcHVzuXbpJDJF/cxwHlfceVUDZ2lnSTw==", "dev": true, "license": "MIT", - "peer": true, "dependencies": { "@eslint-community/eslint-utils": "^4.8.0", "@eslint-community/regexpp": "^4.12.1", @@ -4009,7 +3470,6 @@ "integrity": "sha512-whOE1HFo/qJDyX4SnXzP4N6zOWn79WhnCUY/iDR0mPfQZO8wcYE4JClzI2oZrhBnnMUCBCHZhO6VQyoBU95mZA==", "dev": true, "license": "MIT", - "peer": true, "dependencies": { "@rtsao/scc": "^1.1.0", "array-includes": "^3.1.9", @@ -5667,6 +5127,279 @@ "node": ">= 0.8.0" } }, + "node_modules/lightningcss": { + "version": "1.32.0", + "resolved": "https://registry.npmjs.org/lightningcss/-/lightningcss-1.32.0.tgz", + "integrity": "sha512-NXYBzinNrblfraPGyrbPoD19C1h9lfI/1mzgWYvXUTe414Gz/X1FD2XBZSZM7rRTrMA8JL3OtAaGifrIKhQ5yQ==", + "dev": true, + "license": "MPL-2.0", + "dependencies": { + "detect-libc": "^2.0.3" + }, + "engines": { + "node": ">= 12.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + }, + "optionalDependencies": { + "lightningcss-android-arm64": "1.32.0", + "lightningcss-darwin-arm64": "1.32.0", + "lightningcss-darwin-x64": "1.32.0", + "lightningcss-freebsd-x64": "1.32.0", + "lightningcss-linux-arm-gnueabihf": "1.32.0", + "lightningcss-linux-arm64-gnu": "1.32.0", + "lightningcss-linux-arm64-musl": "1.32.0", + "lightningcss-linux-x64-gnu": "1.32.0", + "lightningcss-linux-x64-musl": "1.32.0", + "lightningcss-win32-arm64-msvc": "1.32.0", + "lightningcss-win32-x64-msvc": "1.32.0" + } + }, + "node_modules/lightningcss-android-arm64": { + "version": "1.32.0", + "resolved": "https://registry.npmjs.org/lightningcss-android-arm64/-/lightningcss-android-arm64-1.32.0.tgz", + "integrity": "sha512-YK7/ClTt4kAK0vo6w3X+Pnm0D2cf2vPHbhOXdoNti1Ga0al1P4TBZhwjATvjNwLEBCnKvjJc2jQgHXH0NEwlAg==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MPL-2.0", + "optional": true, + "os": [ + "android" + ], + "engines": { + "node": ">= 12.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/lightningcss-darwin-arm64": { + "version": "1.32.0", + "resolved": "https://registry.npmjs.org/lightningcss-darwin-arm64/-/lightningcss-darwin-arm64-1.32.0.tgz", + "integrity": "sha512-RzeG9Ju5bag2Bv1/lwlVJvBE3q6TtXskdZLLCyfg5pt+HLz9BqlICO7LZM7VHNTTn/5PRhHFBSjk5lc4cmscPQ==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MPL-2.0", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": ">= 12.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/lightningcss-darwin-x64": { + "version": "1.32.0", + "resolved": "https://registry.npmjs.org/lightningcss-darwin-x64/-/lightningcss-darwin-x64-1.32.0.tgz", + "integrity": "sha512-U+QsBp2m/s2wqpUYT/6wnlagdZbtZdndSmut/NJqlCcMLTWp5muCrID+K5UJ6jqD2BFshejCYXniPDbNh73V8w==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MPL-2.0", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": ">= 12.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/lightningcss-freebsd-x64": { + "version": "1.32.0", + "resolved": "https://registry.npmjs.org/lightningcss-freebsd-x64/-/lightningcss-freebsd-x64-1.32.0.tgz", + "integrity": "sha512-JCTigedEksZk3tHTTthnMdVfGf61Fky8Ji2E4YjUTEQX14xiy/lTzXnu1vwiZe3bYe0q+SpsSH/CTeDXK6WHig==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MPL-2.0", + "optional": true, + "os": [ + "freebsd" + ], + "engines": { + "node": ">= 12.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/lightningcss-linux-arm-gnueabihf": { + "version": "1.32.0", + "resolved": "https://registry.npmjs.org/lightningcss-linux-arm-gnueabihf/-/lightningcss-linux-arm-gnueabihf-1.32.0.tgz", + "integrity": "sha512-x6rnnpRa2GL0zQOkt6rts3YDPzduLpWvwAF6EMhXFVZXD4tPrBkEFqzGowzCsIWsPjqSK+tyNEODUBXeeVHSkw==", + "cpu": [ + "arm" + ], + "dev": true, + "license": "MPL-2.0", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">= 12.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/lightningcss-linux-arm64-gnu": { + "version": "1.32.0", + "resolved": "https://registry.npmjs.org/lightningcss-linux-arm64-gnu/-/lightningcss-linux-arm64-gnu-1.32.0.tgz", + "integrity": "sha512-0nnMyoyOLRJXfbMOilaSRcLH3Jw5z9HDNGfT/gwCPgaDjnx0i8w7vBzFLFR1f6CMLKF8gVbebmkUN3fa/kQJpQ==", + "cpu": [ + "arm64" + ], + "dev": true, + "libc": [ + "glibc" + ], + "license": "MPL-2.0", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">= 12.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/lightningcss-linux-arm64-musl": { + "version": "1.32.0", + "resolved": "https://registry.npmjs.org/lightningcss-linux-arm64-musl/-/lightningcss-linux-arm64-musl-1.32.0.tgz", + "integrity": "sha512-UpQkoenr4UJEzgVIYpI80lDFvRmPVg6oqboNHfoH4CQIfNA+HOrZ7Mo7KZP02dC6LjghPQJeBsvXhJod/wnIBg==", + "cpu": [ + "arm64" + ], + "dev": true, + "libc": [ + "musl" + ], + "license": "MPL-2.0", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">= 12.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/lightningcss-linux-x64-gnu": { + "version": "1.32.0", + "resolved": "https://registry.npmjs.org/lightningcss-linux-x64-gnu/-/lightningcss-linux-x64-gnu-1.32.0.tgz", + "integrity": "sha512-V7Qr52IhZmdKPVr+Vtw8o+WLsQJYCTd8loIfpDaMRWGUZfBOYEJeyJIkqGIDMZPwPx24pUMfwSxxI8phr/MbOA==", + "cpu": [ + "x64" + ], + "dev": true, + "libc": [ + "glibc" + ], + "license": "MPL-2.0", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">= 12.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/lightningcss-linux-x64-musl": { + "version": "1.32.0", + "resolved": "https://registry.npmjs.org/lightningcss-linux-x64-musl/-/lightningcss-linux-x64-musl-1.32.0.tgz", + "integrity": "sha512-bYcLp+Vb0awsiXg/80uCRezCYHNg1/l3mt0gzHnWV9XP1W5sKa5/TCdGWaR/zBM2PeF/HbsQv/j2URNOiVuxWg==", + "cpu": [ + "x64" + ], + "dev": true, + "libc": [ + "musl" + ], + "license": "MPL-2.0", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">= 12.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/lightningcss-win32-arm64-msvc": { + "version": "1.32.0", + "resolved": "https://registry.npmjs.org/lightningcss-win32-arm64-msvc/-/lightningcss-win32-arm64-msvc-1.32.0.tgz", + "integrity": "sha512-8SbC8BR40pS6baCM8sbtYDSwEVQd4JlFTOlaD3gWGHfThTcABnNDBda6eTZeqbofalIJhFx0qKzgHJmcPTnGdw==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MPL-2.0", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">= 12.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/lightningcss-win32-x64-msvc": { + "version": "1.32.0", + "resolved": "https://registry.npmjs.org/lightningcss-win32-x64-msvc/-/lightningcss-win32-x64-msvc-1.32.0.tgz", + "integrity": "sha512-Amq9B/SoZYdDi1kFrojnoqPLxYhQ4Wo5XiL8EVJrVsB8ARoC1PWW6VGtT0WKCemjy8aC+louJnjS7U18x3b06Q==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MPL-2.0", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">= 12.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, "node_modules/lilconfig": { "version": "2.1.0", "resolved": "https://registry.npmjs.org/lilconfig/-/lilconfig-2.1.0.tgz", @@ -5842,9 +5575,9 @@ } }, "node_modules/nanoid": { - "version": "3.3.11", - "resolved": "https://registry.npmjs.org/nanoid/-/nanoid-3.3.11.tgz", - "integrity": "sha512-N8SpfPUnUp1bK+PMYW8qSWdl9U+wwNWI4QKxOYDy9JAro3WMX7p2OeVRF9v+347pnakNevPmiHhNmZ2HbFA76w==", + "version": "3.3.15", + "resolved": "https://registry.npmjs.org/nanoid/-/nanoid-3.3.15.tgz", + "integrity": "sha512-y7Wygv/7mEOvxTuEQDB8StXdMRBWf1kR/tlhAzBRUFkB2jfcLOAxO/SHmOO2zgz1pVgK29/kyupn059/bCHdjA==", "dev": true, "funding": [ { @@ -6301,9 +6034,9 @@ } }, "node_modules/postcss": { - "version": "8.5.12", - "resolved": "https://registry.npmjs.org/postcss/-/postcss-8.5.12.tgz", - "integrity": "sha512-W62t/Se6rA0Az3DfCL0AqJwXuKwBeYg6nOaIgzP+xZ7N5BFCI7DYi1qs6ygUYT6rvfi6t9k65UMLJC+PHZpDAA==", + "version": "8.5.16", + "resolved": "https://registry.npmjs.org/postcss/-/postcss-8.5.16.tgz", + "integrity": "sha512-vuwillviilfKZsg0VGj5R/YwwcHx4SLsIOI/7K6mQkWx+l5cUHTjj5g0AasTBcyXsbfTgrwsUNmVUb5xVwyPwg==", "dev": true, "funding": [ { @@ -6320,9 +6053,8 @@ } ], "license": "MIT", - "peer": true, "dependencies": { - "nanoid": "^3.3.11", + "nanoid": "^3.3.12", "picocolors": "^1.1.1", "source-map-js": "^1.2.1" }, @@ -6657,8 +6389,7 @@ "resolved": "https://registry.npmjs.org/quickjs-wasi/-/quickjs-wasi-2.2.0.tgz", "integrity": "sha512-zQxXmQMrEoD3S+jQdYsloq4qAuaxKFHZj6hHqOYGwB2iQZH+q9e/lf5zQPXCKOk0WJuAjzRFbO4KwHIp2D05Iw==", "dev": true, - "license": "MIT", - "peer": true + "license": "MIT" }, "node_modules/readdirp": { "version": "3.6.0", @@ -6691,8 +6422,7 @@ "resolved": "https://registry.npmjs.org/reflect-metadata/-/reflect-metadata-0.2.2.tgz", "integrity": "sha512-urBwgfrvVP/eAyXx4hluJivBKzuEbSQs9rKWCrCkbSxNv8mxPcUZKeuoF3Uy4mJl3Lwprp6yy5/39VWigZ4K6Q==", "dev": true, - "license": "Apache-2.0", - "peer": true + "license": "Apache-2.0" }, "node_modules/reflect.getprototypeof": { "version": "1.0.10", @@ -6819,49 +6549,38 @@ "url": "https://github.com/privatenumber/resolve-pkg-maps?sponsor=1" } }, - "node_modules/rollup": { - "version": "4.59.0", - "resolved": "https://registry.npmjs.org/rollup/-/rollup-4.59.0.tgz", - "integrity": "sha512-2oMpl67a3zCH9H79LeMcbDhXW/UmWG/y2zuqnF2jQq5uq9TbM9TVyXvA4+t+ne2IIkBdrLpAaRQAvo7YI/Yyeg==", + "node_modules/rolldown": { + "version": "1.1.4", + "resolved": "https://registry.npmjs.org/rolldown/-/rolldown-1.1.4.tgz", + "integrity": "sha512-IjZYiLxZwpnhwhdBH2ugdTGVSdhCQUmLxLoqyjiL0JxYjyRst+5a0P3xfrTxJ5F638j4Mvvw5FAX5XE6eHpXbA==", "dev": true, "license": "MIT", "dependencies": { - "@types/estree": "1.0.8" + "@oxc-project/types": "=0.138.0", + "@rolldown/pluginutils": "^1.0.0" }, "bin": { - "rollup": "dist/bin/rollup" + "rolldown": "bin/cli.mjs" }, "engines": { - "node": ">=18.0.0", - "npm": ">=8.0.0" + "node": "^20.19.0 || >=22.12.0" }, "optionalDependencies": { - "@rollup/rollup-android-arm-eabi": "4.59.0", - "@rollup/rollup-android-arm64": "4.59.0", - "@rollup/rollup-darwin-arm64": "4.59.0", - "@rollup/rollup-darwin-x64": "4.59.0", - "@rollup/rollup-freebsd-arm64": "4.59.0", - "@rollup/rollup-freebsd-x64": "4.59.0", - "@rollup/rollup-linux-arm-gnueabihf": "4.59.0", - "@rollup/rollup-linux-arm-musleabihf": "4.59.0", - "@rollup/rollup-linux-arm64-gnu": "4.59.0", - "@rollup/rollup-linux-arm64-musl": "4.59.0", - "@rollup/rollup-linux-loong64-gnu": "4.59.0", - "@rollup/rollup-linux-loong64-musl": "4.59.0", - "@rollup/rollup-linux-ppc64-gnu": "4.59.0", - "@rollup/rollup-linux-ppc64-musl": "4.59.0", - "@rollup/rollup-linux-riscv64-gnu": "4.59.0", - "@rollup/rollup-linux-riscv64-musl": "4.59.0", - "@rollup/rollup-linux-s390x-gnu": "4.59.0", - "@rollup/rollup-linux-x64-gnu": "4.59.0", - "@rollup/rollup-linux-x64-musl": "4.59.0", - "@rollup/rollup-openbsd-x64": "4.59.0", - "@rollup/rollup-openharmony-arm64": "4.59.0", - "@rollup/rollup-win32-arm64-msvc": "4.59.0", - "@rollup/rollup-win32-ia32-msvc": "4.59.0", - "@rollup/rollup-win32-x64-gnu": "4.59.0", - "@rollup/rollup-win32-x64-msvc": "4.59.0", - "fsevents": "~2.3.2" + "@rolldown/binding-android-arm64": "1.1.4", + "@rolldown/binding-darwin-arm64": "1.1.4", + "@rolldown/binding-darwin-x64": "1.1.4", + "@rolldown/binding-freebsd-x64": "1.1.4", + "@rolldown/binding-linux-arm-gnueabihf": "1.1.4", + "@rolldown/binding-linux-arm64-gnu": "1.1.4", + "@rolldown/binding-linux-arm64-musl": "1.1.4", + "@rolldown/binding-linux-ppc64-gnu": "1.1.4", + "@rolldown/binding-linux-s390x-gnu": "1.1.4", + "@rolldown/binding-linux-x64-gnu": "1.1.4", + "@rolldown/binding-linux-x64-musl": "1.1.4", + "@rolldown/binding-openharmony-arm64": "1.1.4", + "@rolldown/binding-wasm32-wasi": "1.1.4", + "@rolldown/binding-win32-arm64-msvc": "1.1.4", + "@rolldown/binding-win32-x64-msvc": "1.1.4" } }, "node_modules/run-async": { @@ -6880,7 +6599,6 @@ "integrity": "sha512-dhKf903U/PQZY6boNNtAGdWbG85WAbjT/1xYoZIC7FAY0yWapOBQVsVrDl58W86//e1VpMNBtRV4MaXfdMySFA==", "dev": true, "license": "Apache-2.0", - "peer": true, "dependencies": { "tslib": "^2.1.0" } @@ -6959,7 +6677,6 @@ "integrity": "sha512-AaIqGSrjo5lA2Yg7RvFZrlXDBCp3nV4XP73GrLGvdRWWwk+8H3l0SDvq/5bA4eF+0RFPLuWUk3E+P1U/YqnpsQ==", "dev": true, "license": "MIT", - "peer": true, "dependencies": { "chokidar": ">=3.0.0 <4.0.0", "immutable": "^4.0.0", @@ -7419,7 +7136,6 @@ "resolved": "https://registry.npmjs.org/svelte/-/svelte-5.56.3.tgz", "integrity": "sha512-w7JvrM5IFl5cmfbY0TLik9o7mjRUJmRMhOR51tBPu708Gr/MjbGs7VnJnr/B0CaXeI4vtnOh7RKxDr0cwhMdDA==", "license": "MIT", - "peer": true, "dependencies": { "@jridgewell/remapping": "^2.3.4", "@jridgewell/sourcemap-codec": "^1.5.0", @@ -7632,14 +7348,14 @@ "license": "Apache-2.0" }, "node_modules/tinyglobby": { - "version": "0.2.15", - "resolved": "https://registry.npmjs.org/tinyglobby/-/tinyglobby-0.2.15.tgz", - "integrity": "sha512-j2Zq4NyQYG5XMST4cbs02Ak8iJUdxRM0XI5QyxXuZOzKOINmWurp3smXu3y5wDcJrptwpSjgXHzIQxR0omXljQ==", + "version": "0.2.17", + "resolved": "https://registry.npmjs.org/tinyglobby/-/tinyglobby-0.2.17.tgz", + "integrity": "sha512-wXR/dYpcqKmfWpEdZjiKJOwCNFndD0DMnrW/cYjVGttEkBfVgcLFHoNrlj47mjOVic9yyNu65alsgF4NQyTa2g==", "dev": true, "license": "MIT", "dependencies": { "fdir": "^6.5.0", - "picomatch": "^4.0.3" + "picomatch": "^4.0.4" }, "engines": { "node": ">=12.0.0" @@ -7868,7 +7584,6 @@ "integrity": "sha512-jl1vZzPDinLr9eUt3J/t7V6FgNEw9QjvBPdysz9KfQDD41fQrC2Y4vKQdiaUpFT4bXlb1RHhLpp8wtm6M5TgSw==", "dev": true, "license": "Apache-2.0", - "peer": true, "bin": { "tsc": "bin/tsc", "tsserver": "bin/tsserver" @@ -8022,7 +7737,6 @@ "integrity": "sha512-EFrL7Hw4kmhZdwWO3dwwFJo6hO3FXuQ6Bg8BK/faHZ9m1YxqBS31BNSTxklIQkxK/4LlV8zTYnPsIRLBzTzjCA==", "dev": true, "hasInstallScript": true, - "peer": true, "dependencies": { "napi-postinstall": "^0.3.0" }, @@ -8069,19 +7783,17 @@ "license": "MIT" }, "node_modules/vite": { - "version": "7.3.1", - "resolved": "https://registry.npmjs.org/vite/-/vite-7.3.1.tgz", - "integrity": "sha512-w+N7Hifpc3gRjZ63vYBXA56dvvRlNWRczTdmCBBa+CotUzAPf5b7YMdMR/8CQoeYE5LX3W4wj6RYTgonm1b9DA==", + "version": "8.1.3", + "resolved": "https://registry.npmjs.org/vite/-/vite-8.1.3.tgz", + "integrity": "sha512-Ds+gBRbj0lwRO2Y5hwnUBdxSwlAve9LeRyU4sNnAr0ewW0gWF0n5bgXgUzbgZ49MV9BVUAQUFYVcDUcilUExMA==", "dev": true, "license": "MIT", - "peer": true, "dependencies": { - "esbuild": "^0.27.0", - "fdir": "^6.5.0", - "picomatch": "^4.0.3", - "postcss": "^8.5.6", - "rollup": "^4.43.0", - "tinyglobby": "^0.2.15" + "lightningcss": "^1.32.0", + "picomatch": "^4.0.4", + "postcss": "^8.5.16", + "rolldown": "~1.1.3", + "tinyglobby": "^0.2.17" }, "bin": { "vite": "bin/vite.js" @@ -8097,9 +7809,10 @@ }, "peerDependencies": { "@types/node": "^20.19.0 || >=22.12.0", + "@vitejs/devtools": "^0.3.0", + "esbuild": "^0.27.0 || ^0.28.0", "jiti": ">=1.21.0", "less": "^4.0.0", - "lightningcss": "^1.21.0", "sass": "^1.70.0", "sass-embedded": "^1.70.0", "stylus": ">=0.54.8", @@ -8112,13 +7825,16 @@ "@types/node": { "optional": true }, - "jiti": { + "@vitejs/devtools": { "optional": true }, - "less": { + "esbuild": { + "optional": true + }, + "jiti": { "optional": true }, - "lightningcss": { + "less": { "optional": true }, "sass": { @@ -8160,9 +7876,9 @@ } }, "node_modules/vitefu": { - "version": "1.1.1", - "resolved": "https://registry.npmjs.org/vitefu/-/vitefu-1.1.1.tgz", - "integrity": "sha512-B/Fegf3i8zh0yFbpzZ21amWzHmuNlLlmJT6n7bu5e+pCHUKQIfXSYokrqOBGEMMe9UG2sostKQF9mml/vYaWJQ==", + "version": "1.1.3", + "resolved": "https://registry.npmjs.org/vitefu/-/vitefu-1.1.3.tgz", + "integrity": "sha512-ub4okH7Z5KLjb6hDyjqrGXqWtWvoYdU3IGm/NorpgHncKoLTCfRIbvlhBm7r0YstIaQRYlp4yEbFqDcKSzXSSg==", "dev": true, "license": "MIT", "workspaces": [ @@ -8171,7 +7887,7 @@ "tests/projects/workspace/packages/*" ], "peerDependencies": { - "vite": "^3.0.0 || ^4.0.0 || ^5.0.0 || ^6.0.0 || ^7.0.0-beta.0" + "vite": "^3.0.0 || ^4.0.0 || ^5.0.0 || ^6.0.0 || ^7.0.0 || ^8.0.0" }, "peerDependenciesMeta": { "vite": { diff --git a/warpgate-web/package.json b/warpgate-web/package.json index 7bb004289..4b7b148fb 100644 --- a/warpgate-web/package.json +++ b/warpgate-web/package.json @@ -30,7 +30,7 @@ "@otplib/plugin-crypto-js": "^12.0.1", "@otplib/preset-browser": "^12.0.1", "@stylistic/eslint-plugin": "^5.10.0", - "@sveltejs/vite-plugin-svelte": "^6.2.4", + "@sveltejs/vite-plugin-svelte": "^7.1.2", "@sveltestrap/sveltestrap": "^7", "@tsconfig/svelte": "^5.0.8", "@types/qrcode": "^1.5.6", @@ -65,7 +65,7 @@ "typescript": "^5.9.3", "typescript-eslint": "^8.61.0", "ua-parser-js": "^2.0.10", - "vite": "^7.3.1", + "vite": "^8.1.3", "vite-tsconfig-paths": "^6.1.1", "zmodem.js": "^0.1.10" }, From 878eabeb55de6e7e1a746cd85e70ae387cc9c0a3 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Sun, 5 Jul 2026 23:33:38 +0200 Subject: [PATCH 261/556] Bump docker/login-action from 4.2.0 to 4.3.0 (#2139) Signed-off-by: dependabot[bot] --- .github/workflows/docker.yml | 4 ++-- .github/workflows/helm-publish.yaml | 2 +- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/.github/workflows/docker.yml b/.github/workflows/docker.yml index e82a88bea..e37e19508 100644 --- a/.github/workflows/docker.yml +++ b/.github/workflows/docker.yml @@ -47,7 +47,7 @@ jobs: - name: Log into registry ${{ env.REGISTRY }} if: github.event_name != 'pull_request' || github.event.pull_request.head.repo.full_name == github.repository - uses: docker/login-action@650006c6eb7dba73a995cc03b0b2d7f5ca915bee + uses: docker/login-action@c99871dec2022cc055c062a10cc1a1310835ceb4 with: registry: ${{ env.REGISTRY }} username: ${{ github.actor }} @@ -127,7 +127,7 @@ jobs: uses: docker/setup-buildx-action@d7f5e7f509e45cec5c76c4d5afdd7de93d0b3df5 - name: Log into registry ${{ env.REGISTRY }} - uses: docker/login-action@650006c6eb7dba73a995cc03b0b2d7f5ca915bee + uses: docker/login-action@c99871dec2022cc055c062a10cc1a1310835ceb4 with: registry: ${{ env.REGISTRY }} username: ${{ github.actor }} diff --git a/.github/workflows/helm-publish.yaml b/.github/workflows/helm-publish.yaml index 67599a0ca..7f07aa863 100644 --- a/.github/workflows/helm-publish.yaml +++ b/.github/workflows/helm-publish.yaml @@ -62,7 +62,7 @@ jobs: cat ${{ env.CHART_PATH }}/Chart.yaml - name: Log into registry ${{ env.REGISTRY }} - uses: docker/login-action@650006c6eb7dba73a995cc03b0b2d7f5ca915bee + uses: docker/login-action@c99871dec2022cc055c062a10cc1a1310835ceb4 with: registry: ${{ env.REGISTRY }} username: ${{ github.actor }} From 4c14459a65db16ba3654a0908960b266d5bdac98 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Sun, 5 Jul 2026 23:33:48 +0200 Subject: [PATCH 262/556] Bump docker/metadata-action from 6.1.0 to 6.2.0 (#2137) Signed-off-by: dependabot[bot] --- .github/workflows/docker.yml | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/.github/workflows/docker.yml b/.github/workflows/docker.yml index e37e19508..dfed5b5f9 100644 --- a/.github/workflows/docker.yml +++ b/.github/workflows/docker.yml @@ -55,7 +55,7 @@ jobs: - name: Docker meta id: meta - uses: docker/metadata-action@80c7e94dd9b9319bd5eb7a0e0fe9291e23a2a2e9 + uses: docker/metadata-action@dc802804100637a589fabce1cb79ff13a1411302 with: images: ${{ env.REGISTRY }}/${{ env.IMAGE_NAME }} @@ -135,7 +135,7 @@ jobs: - name: Docker meta id: meta - uses: docker/metadata-action@80c7e94dd9b9319bd5eb7a0e0fe9291e23a2a2e9 + uses: docker/metadata-action@dc802804100637a589fabce1cb79ff13a1411302 with: images: ${{ env.REGISTRY }}/${{ env.IMAGE_NAME }} tags: | From cbb11e5507a926dfa0b31b56c1adf92a024ed030 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Sun, 5 Jul 2026 23:33:55 +0200 Subject: [PATCH 263/556] Bump docker/build-push-action from 7.2.0 to 7.3.0 (#2132) Signed-off-by: dependabot[bot] --- .github/workflows/docker.yml | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/.github/workflows/docker.yml b/.github/workflows/docker.yml index dfed5b5f9..e6cea3d11 100644 --- a/.github/workflows/docker.yml +++ b/.github/workflows/docker.yml @@ -62,7 +62,7 @@ jobs: - name: Build Docker image without pushing if: github.event_name == 'pull_request' && github.event.pull_request.head.repo.full_name != github.repository id: build-no-push - uses: docker/build-push-action@f9f3042f7e2789586610d6e8b85c8f03e5195baf + uses: docker/build-push-action@53b7df96c91f9c12dcc8a07bcb9ccacbed38856a with: file: docker/Dockerfile context: . @@ -74,7 +74,7 @@ jobs: - name: Build and push Docker image if: github.event_name != 'pull_request' || github.event.pull_request.head.repo.full_name == github.repository id: build - uses: docker/build-push-action@f9f3042f7e2789586610d6e8b85c8f03e5195baf + uses: docker/build-push-action@53b7df96c91f9c12dcc8a07bcb9ccacbed38856a with: file: docker/Dockerfile context: . From 18bf3f4dd7ca344893e6dbc1a959d1b681e4a0ee Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Sun, 5 Jul 2026 23:34:16 +0200 Subject: [PATCH 264/556] Bump form-data from 4.0.5 to 4.0.6 in /warpgate-web (#2085) Signed-off-by: dependabot[bot] --- warpgate-web/package-lock.json | 16 ++++++++-------- 1 file changed, 8 insertions(+), 8 deletions(-) diff --git a/warpgate-web/package-lock.json b/warpgate-web/package-lock.json index 55ec94acd..400a39664 100644 --- a/warpgate-web/package-lock.json +++ b/warpgate-web/package-lock.json @@ -4058,17 +4058,17 @@ } }, "node_modules/form-data": { - "version": "4.0.5", - "resolved": "https://registry.npmjs.org/form-data/-/form-data-4.0.5.tgz", - "integrity": "sha512-8RipRLol37bNs2bhoV67fiTEvdTrbMUYcFTiy3+wuuOnUog2QBHCZWXDRijWQfAkhBj2Uf5UnVaiWwA5vdd82w==", + "version": "4.0.6", + "resolved": "https://registry.npmjs.org/form-data/-/form-data-4.0.6.tgz", + "integrity": "sha512-vKatAh4SlVfgbv+YtmhiRjhEMJsYpsG1Y2rMQtR+SVSbytsSD1YGzDIcrAJmdFec88u/+VoGmxnl+80gL1tRCQ==", "dev": true, "license": "MIT", "dependencies": { "asynckit": "^0.4.0", "combined-stream": "^1.0.8", "es-set-tostringtag": "^2.1.0", - "hasown": "^2.0.2", - "mime-types": "^2.1.12" + "hasown": "^2.0.4", + "mime-types": "^2.1.35" }, "engines": { "node": ">= 6" @@ -4416,9 +4416,9 @@ } }, "node_modules/hasown": { - "version": "2.0.2", - "resolved": "https://registry.npmjs.org/hasown/-/hasown-2.0.2.tgz", - "integrity": "sha512-0hJU9SCPvmMzIBdZFqNPXWa6dqh7WdH0cII9y+CyS8rG3nL48Bclra9HmKhVVUHyPWNH5Y7xDwAB7bfgSjkUMQ==", + "version": "2.0.4", + "resolved": "https://registry.npmjs.org/hasown/-/hasown-2.0.4.tgz", + "integrity": "sha512-T2UbfbBEF32wiepXIsMlTW9+dDYC6wMh/t/vYA4tuOMKqWz/n3vr1NFSxQiyP+zk2mXsoMA/i/7qV6LKut1t1A==", "dev": true, "license": "MIT", "dependencies": { From f4441cf869adfdde3467b1424c66e09e98623e5e Mon Sep 17 00:00:00 2001 From: Victor Coutellier Date: Mon, 6 Jul 2026 00:05:24 +0200 Subject: [PATCH 265/556] Add ability to use OIDC for Kubernetes target (#2094) Co-authored-by: Claude Opus 4.8 Co-authored-by: Eugene --- Cargo.lock | 1 + config-schema.json | 41 ++ tests/conftest.py | 11 + tests/test_kubernetes_integration.py | 583 +++++++++++++++++- warpgate-core/src/config_providers/mod.rs | 2 + .../src/config_providers/sso_user.rs | 103 ++++ .../src/api/sso_provider_list.rs | 150 +++-- warpgate-protocol-kubernetes/Cargo.toml | 1 + .../src/server/auth.rs | 172 ++++-- warpgate-sso/src/config.rs | 23 + warpgate-sso/src/lib.rs | 2 + warpgate-sso/src/metadata.rs | 50 ++ warpgate-sso/src/request.rs | 118 ++-- warpgate-sso/src/sso.rs | 106 +++- warpgate-web/src/admin/config/SSHKeys.svelte | 9 +- .../targets/ssh/KeyCheckerResult.svelte | 27 +- .../src/admin/lib/openapi-schema.json | 7 +- .../src/common/ConnectionInstructions.svelte | 354 +++++------ .../src/common/CopyableTextArea.svelte | 47 ++ warpgate-web/src/common/protocols.ts | 50 ++ .../src/gateway/ApiTokenManager.svelte | 14 +- .../src/gateway/lib/openapi-schema.json | 64 +- 22 files changed, 1508 insertions(+), 427 deletions(-) create mode 100644 warpgate-core/src/config_providers/sso_user.rs create mode 100644 warpgate-sso/src/metadata.rs create mode 100644 warpgate-web/src/common/CopyableTextArea.svelte diff --git a/Cargo.lock b/Cargo.lock index 3a2f25b17..f1e9e5344 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -8504,6 +8504,7 @@ dependencies = [ "warpgate-common-http", "warpgate-core", "warpgate-db-entities", + "warpgate-sso", "warpgate-tls", ] diff --git a/config-schema.json b/config-schema.json index d51f48dc0..78eb65b5a 100644 --- a/config-schema.json +++ b/config-schema.json @@ -695,6 +695,18 @@ "default_credential_policy": { "description": "Default credential policy for auto-created users.\nKeys: \"http\", \"ssh\", \"mysql\", \"postgres\"\nValues: list of credential kinds e.g. [\"sso\"], [\"web\"], []" }, + "kubernetes": { + "description": "kubectl OIDC parameters for generating a kubelogin kubeconfig.", + "anyOf": [ + { + "$ref": "#/$defs/SsoProviderKubernetesConfig" + }, + { + "type": "null" + } + ], + "default": null + }, "label": { "type": [ "string", @@ -730,6 +742,35 @@ "provider" ] }, + "SsoProviderKubernetesConfig": { + "type": "object", + "properties": { + "client_id": { + "description": "Public OIDC client id used by kubectl (kubelogin). Must be listed in the\nprovider's `additional_trusted_audiences`.", + "type": "string" + }, + "client_secret": { + "description": "Optional client secret (only for confidential kubectl clients).", + "type": [ + "string", + "null" + ] + }, + "scopes": { + "description": "Extra scopes for kubelogin. Defaults to openid/email/profile when unset.", + "type": [ + "array", + "null" + ], + "items": { + "type": "string" + } + } + }, + "required": [ + "client_id" + ] + }, "SsoProviderReturnUrlPrefix": { "type": "string", "enum": [ diff --git a/tests/conftest.py b/tests/conftest.py index 78c64c01a..0c3ae73a3 100644 --- a/tests/conftest.py +++ b/tests/conftest.py @@ -542,6 +542,7 @@ def start_oidc_server( users_override=None, extra_identity_resources=None, redirect_uris=None, + extra_clients=None, ): port = alloc_port() container_name = f"warpgate-e2e-oidc-mock-{uuid.uuid4()}" @@ -567,12 +568,22 @@ def start_oidc_server( "AllowedGrantTypes": ["authorization_code"], "AllowedScopes": allowed_scopes, "ClientClaimsPrefix": "", + # Emit identity-resource claims (email, preferred_username, + # warpgate_roles, ...) directly in the ID token in addition to + # the userinfo endpoint. This is required by the Kubernetes + # OIDC-Bearer auth path, which validates a raw ID token and does + # not call userinfo. Harmless for the interactive flows that + # also read claims from userinfo. + "AlwaysIncludeUserClaimsInIdToken": True, "RedirectUris": redirect_uris or [ f"https://127.0.0.1:{warpgate_http_port}/@warpgate/api/sso/return" ], } ] + if extra_clients: + clients_config.extend(extra_clients) + clients_config_path = oidc_data_dir / "clients-config.json" with open(clients_config_path, "w") as f: _json.dump(clients_config, f) diff --git a/tests/test_kubernetes_integration.py b/tests/test_kubernetes_integration.py index 6f6dfdde1..c2f4a6c2d 100644 --- a/tests/test_kubernetes_integration.py +++ b/tests/test_kubernetes_integration.py @@ -1,7 +1,15 @@ from datetime import datetime, timezone, timedelta +import base64 +import hashlib +import html +import re +import secrets import time import uuid import subprocess +from urllib.parse import parse_qs, urlencode, urlparse + +import requests from cryptography.hazmat.primitives import serialization from cryptography.hazmat.primitives.asymmetric import rsa @@ -10,7 +18,8 @@ import pytest from .api_client import admin_client, sdk -from .conftest import WarpgateProcess, K3sInstance +from .conftest import ProcessManager, WarpgateProcess, K3sInstance +from .util import alloc_port, wait_port def run_kubectl(args, **kwargs): @@ -19,6 +28,237 @@ def run_kubectl(args, **kwargs): ) +# --------------------------------------------------------------------------- +# OIDC helpers +# --------------------------------------------------------------------------- + +# Mirrors the client registered by conftest.start_oidc_server. +OIDC_CLIENT_ID = "warpgate-test" +OIDC_CLIENT_SECRET = "warpgate-test-secret" +# A second OIDC client that simulates kubectl's own client-id. +# Its tokens have aud == KUBECTL_CLIENT_ID (not warpgate-test). +KUBECTL_CLIENT_ID = "kubectl-client" +KUBECTL_CLIENT_SECRET = "kubectl-client-secret" +# Used as the OIDC redirect_uri for our own (non-warpgate) authorization-code +# flow. We register it explicitly with the mock so the token exchange below +# validates the redirect_uri. Warpgate never sees this URL. The mock's +# redirect-uri validator requires an https URL with an explicit port, so we +# derive it from an allocated port (a real listener is never needed). +def _oidc_test_redirect_uri(port): + return f"https://127.0.0.1:{port}/oidc-test-callback" + + +def _make_oidc_sso_provider_config( + oidc_port, + *, + auto_create_users=False, + role_mappings=None, + additional_trusted_audiences=None, +): + """Build an ``sso_providers`` config entry pointing at the OIDC mock. + + Mirrors ``_make_sso_provider_config`` in ``test_http_user_auth_oidc.py`` but + kept local to avoid cross-test coupling. + """ + provider = { + "type": "custom", + "client_id": OIDC_CLIENT_ID, + "client_secret": OIDC_CLIENT_SECRET, + "issuer_url": f"http://localhost:{oidc_port}", + "scopes": [ + "openid", + "email", + "profile", + "preferred_username", + "warpgate_roles", + ], + } + if role_mappings is not None: + provider["role_mappings"] = role_mappings + if additional_trusted_audiences is not None: + provider["additional_trusted_audiences"] = additional_trusted_audiences + return { + "name": "test-oidc", + "label": "OIDC Test", + "provider": provider, + "auto_create_users": auto_create_users, + # Opt this provider into Kubernetes OIDC bearer auth. The auth path is + # gated on the presence of this block; the client_id is only used for + # kubeconfig generation, not for token validation. + "kubernetes": { + "client_id": KUBECTL_CLIENT_ID, + }, + } + + +def _obtain_oidc_id_token( + oidc_port, + redirect_uri, + *, + username="User1", + password="pwd", + client_id=OIDC_CLIENT_ID, + client_secret=OIDC_CLIENT_SECRET, +): + """Drive a self-contained OIDC authorization-code flow against the mock and + return a raw ID token (JWT string). + + This intentionally does NOT go through Warpgate's ``/sso/start`` endpoint: + we run our own authorization request (with our own ``redirect_uri`` and a + self-managed PKCE pair) so we can intercept the authorization ``code`` and + exchange it ourselves at the token endpoint. The resulting token has + ``aud == `` (default: ``warpgate-test``), which is what + Warpgate's Kubernetes Bearer-auth path validates against. + """ + issuer = f"http://localhost:{oidc_port}" + disco = requests.get( + f"{issuer}/.well-known/openid-configuration", timeout=10 + ).json() + authorization_endpoint = disco["authorization_endpoint"] + token_endpoint = disco["token_endpoint"] + + session = requests.Session() + + # The mock client requires PKCE, so generate a verifier/challenge pair. + code_verifier = ( + base64.urlsafe_b64encode(secrets.token_bytes(32)).rstrip(b"=").decode() + ) + code_challenge = ( + base64.urlsafe_b64encode( + hashlib.sha256(code_verifier.encode()).digest() + ) + .rstrip(b"=") + .decode() + ) + + # 1. Authorization request -> mock login page + auth_params = { + "client_id": client_id, + "redirect_uri": redirect_uri, + "response_type": "code", + "scope": "openid email profile preferred_username warpgate_roles", + "state": uuid.uuid4().hex, + "nonce": uuid.uuid4().hex, + "code_challenge": code_challenge, + "code_challenge_method": "S256", + } + auth_url = f"{authorization_endpoint}?{urlencode(auth_params)}" + resp = session.get(auth_url) + assert resp.status_code == 200, ( + f"authorize failed: {resp.status_code} {resp.text[:300]}" + ) + + login_page_url = resp.url + login_html = resp.text + + token_match = re.search( + r'name="__RequestVerificationToken"[^>]*value="([^"]*)"', + login_html, + ) + assert token_match, f"no anti-forgery token in login form: {login_html[:300]}" + verification_token = html.unescape(token_match.group(1)) + + m = re.search(r'name="Input.ReturnUrl"[^>]*value="([^"]*)"', login_html) + assert m, "no ReturnUrl in login form" + return_url = html.unescape(m.group(1)) + + # 2. Submit credentials + resp = session.post( + login_page_url, + data={ + "Input.Username": username, + "Input.Password": password, + "Input.Button": "login", + "Input.ReturnUrl": return_url, + "__RequestVerificationToken": verification_token, + }, + allow_redirects=False, + ) + + # 3. Chase redirects until the mock sends us back to our redirect_uri + code = None + for _ in range(15): + if resp.status_code // 100 != 3: + break + location = resp.headers["Location"] + if location.startswith("/"): + location = f"{issuer}{location}" + if location.startswith(redirect_uri): + qs = parse_qs(urlparse(location).query) + assert "code" in qs, f"no code in callback: {location}" + code = qs["code"][0] + break + resp = session.get(location, allow_redirects=False) + + assert code is not None, ( + "OIDC mock did not redirect back with an authorization code" + ) + + # 4. Exchange the code for tokens + token_resp = requests.post( + token_endpoint, + data={ + "grant_type": "authorization_code", + "code": code, + "redirect_uri": redirect_uri, + "client_id": client_id, + "client_secret": client_secret, + "code_verifier": code_verifier, + }, + timeout=10, + ) + assert token_resp.status_code == 200, ( + f"token exchange failed: {token_resp.status_code} {token_resp.text[:300]}" + ) + body = token_resp.json() + id_token = body.get("id_token") + assert id_token, f"no id_token in token response: {body}" + return id_token + + +def _obtain_kubectl_client_id_token(oidc_port, redirect_uri, *, username="User1", password="pwd"): + """Like _obtain_oidc_id_token but uses KUBECTL_CLIENT_ID as the client. + + The resulting ID token will have ``aud == kubectl-client``, which is NOT + Warpgate's primary client_id (``warpgate-test``). This exercises the + ``additional_trusted_audiences`` path: the token is only accepted when + ``kubectl-client`` is in the provider's ``additional_trusted_audiences`` list. + """ + return _obtain_oidc_id_token( + oidc_port, + redirect_uri, + username=username, + password=password, + client_id=KUBECTL_CLIENT_ID, + client_secret=KUBECTL_CLIENT_SECRET, + ) + + +def _oidc_user_with_roles(roles): + """OIDC mock user config carrying the given warpgate_roles claim values.""" + claims = [ + {"Type": "name", "Value": "Sam Tailor", "ValueType": "string"}, + {"Type": "email", "Value": "sam.tailor@gmail.com", "ValueType": "string"}, + { + "Type": "preferred_username", + "Value": "sam_tailor", + "ValueType": "string", + }, + ] + for r in roles: + claims.append( + {"Type": "warpgate_roles", "Value": r, "ValueType": "string"} + ) + return [ + { + "SubjectId": "1", + "Username": "User1", + "Password": "pwd", + "Claims": claims, + } + ] + + class TestKubernetesIntegration: @pytest.mark.asyncio async def test_kubectl_through_warpgate( @@ -624,3 +864,344 @@ async def test_kubectl_attach_io(self, processes, shared_wg: WarpgateProcess): assert b"hello-from-attach" in p.stdout, ( f"attach stdout did not contain expected text: {p.stdout!r}" ) + + # -- OIDC Bearer authentication ---------------------------------------- + + def _start_oidc_mock_for_roles(self, processes: ProcessManager, roles): + """Start the OIDC mock server pre-configured with a user carrying the + given ``warpgate_roles`` claim values. + + Returns ``(oidc_port, redirect_uri)`` where ``redirect_uri`` is a + registered callback URL suitable for use in the authorization-code flow. + """ + wg_http_port = alloc_port() + redirect_uri = _oidc_test_redirect_uri(alloc_port()) + oidc_port = processes.start_oidc_server( + wg_http_port, + extra_scopes=["warpgate_roles"], + users_override=_oidc_user_with_roles(roles), + extra_identity_resources=[ + {"Name": "warpgate_roles", "ClaimTypes": ["warpgate_roles"]}, + ], + redirect_uris=[redirect_uri], + ) + return oidc_port, redirect_uri + + def _start_oidc_mock_for_roles_with_kubectl_client( + self, processes: ProcessManager, roles + ): + """Like ``_start_oidc_mock_for_roles`` but also registers a second OIDC + client ``kubectl-client`` that simulates the kubectl exec-plugin audience. + + Returns ``(oidc_port, primary_redirect_uri, kubectl_redirect_uri)`` + where ``kubectl_redirect_uri`` is registered for ``KUBECTL_CLIENT_ID``. + """ + wg_http_port = alloc_port() + primary_redirect_uri = _oidc_test_redirect_uri(alloc_port()) + kubectl_redirect_uri = _oidc_test_redirect_uri(alloc_port()) + + # The extra client mirrors warpgate-test but with a different client_id. + # It shares the same allowed scopes and always includes user claims in + # the ID token so the Kubernetes OIDC path can read them without userinfo. + kubectl_client_entry = { + "ClientId": KUBECTL_CLIENT_ID, + "ClientSecrets": [KUBECTL_CLIENT_SECRET], + "AllowedGrantTypes": ["authorization_code"], + "AllowedScopes": [ + "openid", + "profile", + "email", + "preferred_username", + "warpgate_roles", + ], + "ClientClaimsPrefix": "", + "AlwaysIncludeUserClaimsInIdToken": True, + "RedirectUris": [kubectl_redirect_uri], + } + + oidc_port = processes.start_oidc_server( + wg_http_port, + extra_scopes=["warpgate_roles"], + users_override=_oidc_user_with_roles(roles), + extra_identity_resources=[ + {"Name": "warpgate_roles", "ClaimTypes": ["warpgate_roles"]}, + ], + redirect_uris=[primary_redirect_uri], + extra_clients=[kubectl_client_entry], + ) + return oidc_port, primary_redirect_uri, kubectl_redirect_uri + + def _start_wg_and_k3s_target( + self, + processes: ProcessManager, + *, + oidc_port, + role_mappings, + target_role_name, + ): + """Start a dedicated warpgate wired to the OIDC mock and a token-auth + Kubernetes target backed by k3s. + + Returns ``(wg, target_name, k3s)``. A role named ``target_role_name`` + is created, granted access to the target, and used as a mapping value + for the OIDC ``warpgate_roles`` claim per ``role_mappings``. + """ + k3s = processes.start_k3s() + + wg = processes.start_wg( + config_patch={ + "sso_providers": [ + _make_oidc_sso_provider_config( + oidc_port, + auto_create_users=True, + role_mappings=role_mappings, + ) + ], + }, + ) + wait_port(wg.http_port, for_process=wg.process, recv=False) + url = f"https://localhost:{wg.http_port}" + + target_name = f"k8s-oidc-{uuid.uuid4()}" + with admin_client(url) as api: + role = api.create_role( + sdk.RoleDataRequest(name=target_role_name) + ) + target = api.create_target( + sdk.TargetDataRequest( + name=target_name, + options=sdk.TargetOptions( + sdk.TargetOptionsTargetKubernetesOptions( + kind="Kubernetes", + cluster_url=f"https://127.0.0.1:{k3s.port}", + tls=sdk.Tls( + mode=sdk.TlsMode.PREFERRED, verify=False + ), + auth=sdk.KubernetesTargetAuth( + sdk.KubernetesTargetAuthKubernetesTargetTokenAuth( + kind="Token", token=k3s.token + ) + ), + ) + ), + ) + ) + api.add_target_role(target.id, role.id) + + return wg, target_name, k3s + + @pytest.mark.asyncio + async def test_kubectl_oidc_bearer_authenticates( + self, processes: ProcessManager + ): + """A valid OIDC ID token for an authorized user -> 200.""" + target_role = f"k8s-oidc-role-{uuid.uuid4()}" + oidc_port, redirect_uri = self._start_oidc_mock_for_roles( + processes, ["k8s-users"] + ) + + wg, target_name, _k3s = self._start_wg_and_k3s_target( + processes, + oidc_port=oidc_port, + role_mappings={"k8s-users": target_role}, + target_role_name=target_role, + ) + + id_token = _obtain_oidc_id_token(oidc_port, redirect_uri) + + resp = requests.get( + f"https://localhost:{wg.kubernetes_port}/{target_name}/version", + headers={"Authorization": f"Bearer {id_token}"}, + verify=False, + ) + assert resp.status_code == 200, ( + f"expected 200, got {resp.status_code}: {resp.text[:300]}" + ) + + @pytest.mark.asyncio + async def test_kubectl_oidc_invalid_token_rejected( + self, processes: ProcessManager + ): + """A garbage / unverifiable Bearer token -> 401. + + The Rust auth path (auth.rs) only performs target lookup *after* a token + has been validated (as an API token or a verifiable OIDC ID token). A + garbage token exhausts both checks and hits the final 401 before any + target or k3s lookup, so neither k3s nor a Kubernetes target is needed. + """ + oidc_port, _ = self._start_oidc_mock_for_roles(processes, ["k8s-users"]) + + # Start warpgate with an SSO provider so the OIDC path is exercised, + # but skip k3s and target creation — auth fails before target lookup. + wg = processes.start_wg( + config_patch={ + "sso_providers": [ + _make_oidc_sso_provider_config(oidc_port) + ], + }, + ) + wait_port(wg.http_port, for_process=wg.process, recv=False) + + # An unsigned/garbage JWT - neither a valid API token nor a verifiable + # OIDC ID token, so authentication must fail outright. + bad_token = "eyJhbGciOiJub25lIn0.eyJzdWIiOiJub2JvZHkifQ.bogus" + resp = requests.get( + f"https://localhost:{wg.kubernetes_port}/some-target/version", + headers={"Authorization": f"Bearer {bad_token}"}, + verify=False, + ) + assert resp.status_code == 401, ( + f"expected 401, got {resp.status_code}: {resp.text[:300]}" + ) + + @pytest.mark.asyncio + async def test_kubectl_oidc_unauthorized_user_forbidden( + self, processes: ProcessManager + ): + """A valid OIDC token whose roles do NOT grant access -> 403.""" + target_role = f"k8s-oidc-role-{uuid.uuid4()}" + # The OIDC user carries "other-team", but the target only grants the + # role mapped from "k8s-users". "other-team" maps to an unrelated role + # that is never authorized on the target, so the user authenticates but + # is not authorized. + unrelated_role = f"k8s-oidc-unrelated-{uuid.uuid4()}" + oidc_port, redirect_uri = self._start_oidc_mock_for_roles( + processes, ["other-team"] + ) + + wg, target_name, _k3s = self._start_wg_and_k3s_target( + processes, + oidc_port=oidc_port, + role_mappings={ + "k8s-users": target_role, + "other-team": unrelated_role, + }, + target_role_name=target_role, + ) + # Make sure the unrelated role exists so the mapping resolves but it is + # never granted on the target. + with admin_client(f"https://localhost:{wg.http_port}") as api: + api.create_role(sdk.RoleDataRequest(name=unrelated_role)) + + id_token = _obtain_oidc_id_token(oidc_port, redirect_uri) + + resp = requests.get( + f"https://localhost:{wg.kubernetes_port}/{target_name}/version", + headers={"Authorization": f"Bearer {id_token}"}, + verify=False, + ) + assert resp.status_code == 403, ( + f"expected 403, got {resp.status_code}: {resp.text[:300]}" + ) + + @pytest.mark.asyncio + async def test_kubectl_oidc_trusted_audience_accepted( + self, processes: ProcessManager + ): + """A valid OIDC ID token whose aud is a separately-registered kubectl + client (not Warpgate's own client_id) is accepted when that client id + is listed in ``additional_trusted_audiences``. + + The OIDC mock issues a token with ``aud == kubectl-client``; Warpgate's + primary ``client_id`` is ``warpgate-test``. Because ``kubectl-client`` + is in ``additional_trusted_audiences``, the token must be accepted (200). + """ + target_role = f"k8s-oidc-role-{uuid.uuid4()}" + oidc_port, primary_redirect_uri, kubectl_redirect_uri = ( + self._start_oidc_mock_for_roles_with_kubectl_client( + processes, ["k8s-users"] + ) + ) + + k3s = processes.start_k3s() + + # Warpgate config: client_id = warpgate-test, but kubectl-client is trusted. + wg = processes.start_wg( + config_patch={ + "sso_providers": [ + _make_oidc_sso_provider_config( + oidc_port, + auto_create_users=True, + role_mappings={"k8s-users": target_role}, + additional_trusted_audiences=[KUBECTL_CLIENT_ID], + ) + ], + }, + ) + wait_port(wg.http_port, for_process=wg.process, recv=False) + url = f"https://localhost:{wg.http_port}" + + target_name = f"k8s-oidc-trusted-aud-{uuid.uuid4()}" + with admin_client(url) as api: + role = api.create_role(sdk.RoleDataRequest(name=target_role)) + target = api.create_target( + sdk.TargetDataRequest( + name=target_name, + options=sdk.TargetOptions( + sdk.TargetOptionsTargetKubernetesOptions( + kind="Kubernetes", + cluster_url=f"https://127.0.0.1:{k3s.port}", + tls=sdk.Tls(mode=sdk.TlsMode.PREFERRED, verify=False), + auth=sdk.KubernetesTargetAuth( + sdk.KubernetesTargetAuthKubernetesTargetTokenAuth( + kind="Token", token=k3s.token + ) + ), + ) + ), + ) + ) + api.add_target_role(target.id, role.id) + + # Obtain a token issued for kubectl-client (aud == kubectl-client). + id_token = _obtain_kubectl_client_id_token(oidc_port, kubectl_redirect_uri) + + resp = requests.get( + f"https://localhost:{wg.kubernetes_port}/{target_name}/version", + headers={"Authorization": f"Bearer {id_token}"}, + verify=False, + ) + assert resp.status_code == 200, ( + f"expected 200 for trusted audience, got {resp.status_code}: {resp.text[:300]}" + ) + + @pytest.mark.asyncio + async def test_kubectl_oidc_untrusted_audience_rejected( + self, processes: ProcessManager + ): + """A valid OIDC ID token whose aud is NOT in ``additional_trusted_audiences`` + and is NOT Warpgate's ``client_id`` must be rejected (401). + + This is the negative case for the trusted-audience path: ``kubectl-client`` + is a registered OIDC client (so the token is cryptographically valid), but + it is NOT listed in ``additional_trusted_audiences``. Authentication must + fail before any target lookup. + """ + oidc_port, _primary_redirect_uri, kubectl_redirect_uri = ( + self._start_oidc_mock_for_roles_with_kubectl_client( + processes, ["k8s-users"] + ) + ) + + # Warpgate: client_id = warpgate-test, NO additional_trusted_audiences. + wg = processes.start_wg( + config_patch={ + "sso_providers": [ + _make_oidc_sso_provider_config(oidc_port) + # additional_trusted_audiences intentionally omitted + ], + }, + ) + wait_port(wg.http_port, for_process=wg.process, recv=False) + + # Token with aud == kubectl-client (valid JWT, but wrong audience). + id_token = _obtain_kubectl_client_id_token(oidc_port, kubectl_redirect_uri) + + resp = requests.get( + f"https://localhost:{wg.kubernetes_port}/some-target/version", + headers={"Authorization": f"Bearer {id_token}"}, + verify=False, + ) + assert resp.status_code == 401, ( + f"expected 401 for untrusted audience, got {resp.status_code}: {resp.text[:300]}" + ) diff --git a/warpgate-core/src/config_providers/mod.rs b/warpgate-core/src/config_providers/mod.rs index d712ce37f..2b240099d 100644 --- a/warpgate-core/src/config_providers/mod.rs +++ b/warpgate-core/src/config_providers/mod.rs @@ -1,10 +1,12 @@ mod db; +mod sso_user; use std::sync::Arc; pub use db::DatabaseConfigProvider; use enum_dispatch::enum_dispatch; use sea_orm::ActiveValue::Set; use sea_orm::{ActiveModelTrait, ColumnTrait, DatabaseConnection, EntityTrait, QueryFilter}; +pub use sso_user::resolve_and_map_sso_user; use time::OffsetDateTime; use tokio::sync::Mutex; use tracing::warn; diff --git a/warpgate-core/src/config_providers/sso_user.rs b/warpgate-core/src/config_providers/sso_user.rs new file mode 100644 index 000000000..6f3b55595 --- /dev/null +++ b/warpgate-core/src/config_providers/sso_user.rs @@ -0,0 +1,103 @@ +use tracing::debug; +use warpgate_common::WarpgateError; +use warpgate_common::auth::AuthCredential; +use warpgate_sso::{RoleMapping, SsoLoginResponse, SsoProviderConfig}; + +use crate::ConfigProvider; + +/// Resolve the Warpgate username for a verified SSO response (creating the user +/// if `auto_create_users` is set), then apply role and admin-role mappings. +/// Returns `None` when no user matches and auto-create is disabled. +pub async fn resolve_and_map_sso_user( + cp: &mut C, + provider_config: &SsoProviderConfig, + response: &SsoLoginResponse, +) -> Result, WarpgateError> { + let Some(email) = response.email.clone() else { + return Ok(None); + }; + if response.email_verified == Some(false) { + tracing::warn!("Rejecting SSO user with explicitly unverified email"); + return Ok(None); + } + + let cred = AuthCredential::Sso { + provider: provider_config.name.clone(), + email, + }; + + let Some(username) = cp + .username_for_sso_credential( + &cred, + response.preferred_username.clone(), + provider_config.clone(), + ) + .await? + else { + return Ok(None); + }; + + let mappings = provider_config.provider.role_mappings(); + if let Some(remote_groups) = response.access_roles.clone() { + let managed_role_names = mappings + .as_ref() + .map(|m| m.iter().flat_map(|(_, v)| v.roles()).collect::>()); + + let mut active_role_names: Vec = if let Some(ref mappings) = mappings { + let mut roles: Vec = if remote_groups.is_empty() { + Vec::new() + } else { + mappings + .get("*") + .map(RoleMapping::roles) + .unwrap_or_default() + }; + for group in &remote_groups { + if let Some(mapping) = mappings.get(group) { + roles.extend(mapping.roles()); + } + } + roles + } else { + remote_groups + }; + active_role_names.sort(); + active_role_names.dedup(); + + debug!( + "SSO role mappings for {username}: active={active_role_names:?}, managed={managed_role_names:?}" + ); + cp.apply_sso_role_mappings(&username, managed_role_names, active_role_names) + .await?; + } + + if let Some(remote_admins) = response.admin_roles.clone() { + let admin_map = provider_config.provider.admin_role_mappings(); + let managed_admin_names: Option> = admin_map + .as_ref() + .map(|m| m.values().flat_map(RoleMapping::roles).collect()); + + let active_admin_names: Vec = if let Some(ref mappings) = admin_map { + remote_admins + .iter() + .flat_map(|r| { + mappings + .get(r) + .map(RoleMapping::roles) + .into_iter() + .flatten() + }) + .collect() + } else { + remote_admins + }; + + debug!( + "SSO admin role mappings for {username}: active={active_admin_names:?}, managed={managed_admin_names:?}" + ); + cp.apply_sso_admin_role_mappings(&username, managed_admin_names, active_admin_names) + .await?; + } + + Ok(Some(username)) +} diff --git a/warpgate-protocol-http/src/api/sso_provider_list.rs b/warpgate-protocol-http/src/api/sso_provider_list.rs index 971969cb5..36796013e 100644 --- a/warpgate-protocol-http/src/api/sso_provider_list.rs +++ b/warpgate-protocol-http/src/api/sso_provider_list.rs @@ -11,17 +11,19 @@ use tokio::sync::Mutex; use tracing::{debug, error, info, warn}; use warpgate_common::WarpgateError; use warpgate_common::auth::{AuthCredential, AuthResult}; -use warpgate_common_http::auth::UnauthenticatedRequestContext; +use warpgate_common_http::auth::{AuthenticatedRequestContext, UnauthenticatedRequestContext}; use warpgate_common_http::ext::construct_external_url; use warpgate_core::ConfigProvider; use warpgate_core::auth::validate_and_add_credential; -use warpgate_sso::{RoleMapping, SsoClient, SsoInternalProviderConfig}; +use warpgate_sso::{SsoClient, SsoInternalProviderConfig}; use super::sso_provider_detail::{SSO_CONTEXT_SESSION_KEY, SsoContext}; use crate::SsoLoginState; +use crate::api::AnySecurityScheme; use crate::api::common::{emit_unknown_authentication_failed_event, logout}; use crate::common::{ - SessionExt, authorize_session, get_or_create_auth_state_for_request, session_id_for_request, + SessionExt, authorize_session, endpoint_auth, get_or_create_auth_state_for_request, + session_id_for_request, }; use crate::session::SessionStore; @@ -84,6 +86,22 @@ enum StartSloResponse { NotFound, } +#[derive(Object)] +pub struct SsoKubernetesConfigDescription { + pub name: String, + pub label: String, + pub issuer_url: String, + pub client_id: String, + pub scopes: Vec, + pub client_secret: Option, +} + +#[derive(ApiResponse)] +enum GetSsoKubernetesConfigsResponse { + #[oai(status = 200)] + Ok(Json>), +} + fn make_redirect_url(err: &str) -> String { error!("SSO error: {err}"); format!("/@warpgate?login_error={err}") @@ -247,7 +265,7 @@ impl Api { return Ok(Err("The SSO account's e-mail is not verified".to_string())); } - let Some(email) = response.email else { + let Some(ref email) = response.email else { return Ok(Err("No e-mail information in the SSO response".to_string())); }; @@ -277,7 +295,7 @@ impl Api { .await .username_for_sso_credential( &cred, - response.preferred_username, + response.preferred_username.clone(), provider_config.clone(), ) .await?; @@ -286,7 +304,7 @@ impl Api { emit_unknown_authentication_failed_event( session_id, req.remote_addr().as_socket_addr().map(|a| a.ip()), - &email, + email, &cred.safe_description(), "unknown user", ); @@ -343,85 +361,17 @@ impl Api { .await; session.set_sso_login_state(SsoLoginState { provider: context.provider, - token: response.id_token, + token: response.id_token.clone(), supports_single_logout: context.supports_single_logout, }); } - let mut cp = services.config_provider.lock().await; - - let mappings = provider_config.provider.role_mappings(); - if let Some(remote_groups) = response.access_roles { - // If mappings is not set, all groups are subject to sync - // and names won't be remapped - let managed_role_names = mappings - .as_ref() - .map(|m| m.iter().flat_map(|(_, v)| v.roles()).collect::>()); - - let mut active_role_names: Vec = if let Some(ref mappings) = mappings { - // Apply wildcard "*" mapping if user has any groups - let mut roles: Vec = if remote_groups.is_empty() { - Vec::new() - } else { - mappings - .get("*") - .map(RoleMapping::roles) - .unwrap_or_default() - }; - - // Apply specific group mappings - for group in &remote_groups { - if let Some(mapping) = mappings.get(group) { - roles.extend(mapping.roles()); - } - } - - roles - } else { - // No mappings configured, pass through group names as-is - remote_groups - }; - - active_role_names.sort(); - active_role_names.dedup(); - - debug!( - "SSO role mappings for {username}: active={active_role_names:?}, managed={managed_role_names:?}" - ); - cp.apply_sso_role_mappings(&username, managed_role_names, active_role_names) - .await?; - } - - // import admin roles from claim if present - if let Some(remote_admins) = response.admin_roles { - let admin_map = provider_config.provider.admin_role_mappings(); - - // compute managed list from mapping values (or all role names if no mapping provided) - let managed_admin_names: Option> = admin_map - .as_ref() - .map(|m| m.values().flat_map(RoleMapping::roles).collect()); - - let active_admin_names: Vec<_> = if let Some(ref mappings) = admin_map { - remote_admins - .iter() - .flat_map(|r| { - mappings - .get(r) - .map(RoleMapping::roles) - .into_iter() - .flatten() - }) - .collect() - } else { - remote_admins.clone() - }; - - debug!( - "SSO admin role mappings for {username}: active={active_admin_names:?}, managed={managed_admin_names:?}" - ); - cp.apply_sso_admin_role_mappings(&username, managed_admin_names, active_admin_names) - .await?; - } + warpgate_core::resolve_and_map_sso_user( + &mut *services.config_provider.lock().await, + provider_config, + &response, + ) + .await?; let mut next_url = context .next_url @@ -478,6 +428,44 @@ impl Api { url: logout_url.to_string(), }))) } + + #[oai( + path = "/sso/kubernetes-configs", + method = "get", + operation_id = "get_sso_kubernetes_configs", + transform = "endpoint_auth" + )] + async fn api_get_sso_kubernetes_configs( + &self, + ctx: Data<&AuthenticatedRequestContext>, + _sec_scheme: AnySecurityScheme, + ) -> Result { + let mut providers = ctx + .services() + .config + .lock() + .await + .store + .sso_providers + .clone(); + providers.sort_by(|a, b| a.label().cmp(b.label())); + let configs = providers + .iter() + .filter_map(|p| { + let k = p.kubernetes.as_ref()?; + let issuer_url = p.provider.issuer_url().ok()?; + Some(SsoKubernetesConfigDescription { + name: p.name.clone(), + label: p.label().to_string(), + issuer_url: issuer_url.to_string(), + client_id: k.client_id.clone(), + scopes: k.scopes_or_default(), + client_secret: k.client_secret.clone(), + }) + }) + .collect(); + Ok(GetSsoKubernetesConfigsResponse::Ok(Json(configs))) + } } #[cfg(test)] diff --git a/warpgate-protocol-kubernetes/Cargo.toml b/warpgate-protocol-kubernetes/Cargo.toml index e2f97eb9e..2ada9a143 100644 --- a/warpgate-protocol-kubernetes/Cargo.toml +++ b/warpgate-protocol-kubernetes/Cargo.toml @@ -38,4 +38,5 @@ warpgate-common = { path = "../warpgate-common", default-features = false } warpgate-common-http = { path = "../warpgate-common-http", default-features = false } warpgate-core = { path = "../warpgate-core", default-features = false } warpgate-db-entities = { path = "../warpgate-db-entities", default-features = false } +warpgate-sso = { path = "../warpgate-sso", default-features = false } warpgate-tls = { path = "../warpgate-tls", default-features = false } diff --git a/warpgate-protocol-kubernetes/src/server/auth.rs b/warpgate-protocol-kubernetes/src/server/auth.rs index b0bd964e5..88ff21ad5 100644 --- a/warpgate-protocol-kubernetes/src/server/auth.rs +++ b/warpgate-protocol-kubernetes/src/server/auth.rs @@ -24,29 +24,72 @@ pub async fn authenticate_and_get_target( { let mut config_provider = services.config_provider.lock().await; if let Ok(Some(user)) = config_provider.validate_api_token(token).await { - let target = config_provider - .get_target_by_name(target_name) - .await - .context("looking up target")? - .filter(|t| matches!(t.options, TargetOptions::Kubernetes(_))) - .ok_or_else(|| { - poem::Error::from_string( - format!("Kubernetes target not found: {target_name}"), - poem::http::StatusCode::NOT_FOUND, - ) - })?; - - if !config_provider - .authorize_target(&user.username, &target.name) - .await - .unwrap_or(false) + let target = + lookup_authorized_k8s_target(&mut *config_provider, target_name, &user.username) + .await?; + return Ok(((&user).into(), target)); + } + drop(config_provider); + + // API token did not match — try OIDC ID token validation against any SSO + // provider that has opted into Kubernetes OIDC. + let sso_providers = { + let config = services.config.lock().await; + config.store.sso_providers.clone() + }; + + // Routing hint: only a provider whose issuer matches the token can + // verify it, so we avoid issuer-discovery network calls to the others. + let token_issuer = warpgate_sso::unverified_issuer(token); + + for provider_config in sso_providers.iter().filter(|p| p.kubernetes.is_some()) { + if let Some(ref token_issuer) = token_issuer + && let Ok(provider_issuer) = provider_config.provider.issuer_url() + && provider_issuer.url().as_str().trim_end_matches('/') + != token_issuer.trim_end_matches('/') { - return Err(poem::Error::from_string( - format!("Access denied to target: {target_name}"), - poem::http::StatusCode::FORBIDDEN, - )); + continue; } - return Ok(((&user).into(), target)); + + let client = match warpgate_sso::SsoClient::new(provider_config.provider.clone()) { + Ok(c) => c, + Err(e) => { + debug!(provider = %provider_config.name, error = %e, "Skipping SSO provider (client init failed)"); + continue; + } + }; + + let response = match client.verify_id_token_to_response(token).await { + Ok(r) => r, + Err(e) => { + // Wrong issuer / audience / signature for this provider — try the next. + debug!(provider = %provider_config.name, error = %e, "OIDC token not valid for provider"); + continue; + } + }; + + let mut config_provider = services.config_provider.lock().await; + let Some(username) = warpgate_core::resolve_and_map_sso_user( + &mut *config_provider, + provider_config, + &response, + ) + .await + .map_err(|e| { + poem::Error::from_string( + format!("SSO user resolution failed: {e}"), + poem::http::StatusCode::INTERNAL_SERVER_ERROR, + ) + })? + else { + continue; + }; + + let target = + lookup_authorized_k8s_target(&mut *config_provider, target_name, &username).await?; + drop(config_provider); + + return Ok((user_info_for_username(services, &username).await?, target)); } } @@ -59,28 +102,12 @@ pub async fn authenticate_and_get_target( Ok(Some(user_info)) => { // Look up the specific target by name from the URL let mut config_provider = services.config_provider.lock().await; - let target = config_provider - .get_target_by_name(target_name) - .await - .context("looking up target")? - .filter(|t| matches!(t.options, TargetOptions::Kubernetes(_))) - .ok_or_else(|| { - poem::Error::from_string( - format!("Kubernetes target not found: {target_name}"), - poem::http::StatusCode::NOT_FOUND, - ) - })?; - - if !config_provider - .authorize_target(&user_info.username, &target.name) - .await - .unwrap_or(false) - { - return Err(poem::Error::from_string( - format!("Access denied to target: {target_name}"), - poem::http::StatusCode::FORBIDDEN, - )); - } + let target = lookup_authorized_k8s_target( + &mut *config_provider, + target_name, + &user_info.username, + ) + .await?; return Ok((user_info, target)); } Ok(None) => { @@ -101,6 +128,65 @@ pub async fn authenticate_and_get_target( )) } +/// Look up a Kubernetes target by name and ensure `username` is authorized for +/// it. Shared by the API-token, OIDC and client-certificate auth paths. +async fn lookup_authorized_k8s_target( + config_provider: &mut C, + target_name: &str, + username: &str, +) -> poem::Result { + let target = config_provider + .get_target_by_name(target_name) + .await + .context("looking up target")? + .filter(|t| matches!(t.options, TargetOptions::Kubernetes(_))) + .ok_or_else(|| { + poem::Error::from_string( + format!("Kubernetes target not found: {target_name}"), + poem::http::StatusCode::NOT_FOUND, + ) + })?; + + if !config_provider + .authorize_target(username, &target.name) + .await + .unwrap_or(false) + { + return Err(poem::Error::from_string( + format!("Access denied to target: {target_name}"), + poem::http::StatusCode::FORBIDDEN, + )); + } + + Ok(target) +} + +/// Load a resolved SSO user's `AuthStateUserInfo` by username. +async fn user_info_for_username( + services: &Services, + username: &str, +) -> poem::Result { + let db = services.db.lock().await; + let model = warpgate_db_entities::User::Entity::find() + .filter(warpgate_db_entities::User::Entity::username_eq_ci(username)) + .one(&*db) + .await + .context("looking up user in database")? + .ok_or_else(|| { + poem::Error::from_string( + format!("User not found after SSO resolution: {username}"), + poem::http::StatusCode::UNAUTHORIZED, + ) + })?; + let user = User::try_from(model).map_err(|e| { + poem::Error::from_string( + format!("Failed to convert user model: {e}"), + poem::http::StatusCode::INTERNAL_SERVER_ERROR, + ) + })?; + Ok((&user).into()) +} + pub async fn create_authenticated_client( k8s_options: &TargetKubernetesOptions, _auth_user: Option<&String>, diff --git a/warpgate-sso/src/config.rs b/warpgate-sso/src/config.rs index 902921506..c8cb43da6 100644 --- a/warpgate-sso/src/config.rs +++ b/warpgate-sso/src/config.rs @@ -63,6 +63,26 @@ pub enum SsoReturnUrlDomainPreference { HostHeader, } +#[derive(Clone, Debug, Serialize, Deserialize, JsonSchema)] +pub struct SsoProviderKubernetesConfig { + /// Public OIDC client id used by kubectl (kubelogin). Must be listed in the + /// provider's `additional_trusted_audiences`. + pub client_id: String, + /// Extra scopes for kubelogin. Defaults to openid/email/profile when unset. + pub scopes: Option>, + /// Optional client secret (only for confidential kubectl clients). + pub client_secret: Option, +} + +impl SsoProviderKubernetesConfig { + /// kubelogin scopes, falling back to the OIDC defaults when unset. + pub fn scopes_or_default(&self) -> Vec { + self.scopes + .clone() + .unwrap_or_else(|| ["openid", "email", "profile"].map(String::from).to_vec()) + } +} + #[derive(Clone, Debug, Serialize, Deserialize, JsonSchema)] pub struct SsoProviderConfig { pub name: String, @@ -79,6 +99,9 @@ pub struct SsoProviderConfig { /// Keys: "http", "ssh", "mysql", "postgres" /// Values: list of credential kinds e.g. ["sso"], ["web"], [] pub default_credential_policy: Option, + /// kubectl OIDC parameters for generating a kubelogin kubeconfig. + #[serde(default)] + pub kubernetes: Option, } impl SsoProviderConfig { diff --git a/warpgate-sso/src/lib.rs b/warpgate-sso/src/lib.rs index 8e77bc01e..8c6a9bb32 100644 --- a/warpgate-sso/src/lib.rs +++ b/warpgate-sso/src/lib.rs @@ -1,12 +1,14 @@ mod config; mod error; pub(crate) mod google_groups; +mod metadata; mod request; mod response; mod sso; pub use config::*; pub use error::*; +pub use metadata::*; pub use request::*; pub use response::*; pub use sso::*; diff --git a/warpgate-sso/src/metadata.rs b/warpgate-sso/src/metadata.rs new file mode 100644 index 000000000..f24ae4e2d --- /dev/null +++ b/warpgate-sso/src/metadata.rs @@ -0,0 +1,50 @@ +use std::collections::HashMap; +use std::sync::{LazyLock, Mutex}; +use std::time::{Duration, Instant}; + +use openidconnect::{DiscoveryError, ProviderMetadataWithLogout, reqwest}; + +use crate::SsoError; +use crate::config::SsoInternalProviderConfig; + +const METADATA_CACHE_TTL: Duration = Duration::from_secs(300); + +#[allow(clippy::type_complexity)] +static METADATA_CACHE: LazyLock>> = + LazyLock::new(|| Mutex::new(HashMap::new())); + +fn cached_metadata(issuer: &str) -> Option { + let cache = METADATA_CACHE.lock().ok()?; + let (fetched_at, metadata) = cache.get(issuer)?; + (fetched_at.elapsed() < METADATA_CACHE_TTL).then(|| metadata.clone()) +} + +fn store_metadata(issuer: String, metadata: &ProviderMetadataWithLogout) { + if let Ok(mut cache) = METADATA_CACHE.lock() { + cache.insert(issuer, (Instant::now(), metadata.clone())); + } +} + +pub async fn discover_metadata( + config: &SsoInternalProviderConfig, + http_client: &reqwest::Client, +) -> Result { + let issuer = config.issuer_url()?; + let cache_key = issuer.to_string(); + + if let Some(metadata) = cached_metadata(&cache_key) { + return Ok(metadata); + } + + let metadata = ProviderMetadataWithLogout::discover_async(issuer, http_client) + .await + .map_err(|e| { + SsoError::Discovery(match e { + DiscoveryError::Request(inner) => format!("Request error: {inner:?}"), + e => format!("{e}"), + }) + })?; + + store_metadata(cache_key, &metadata); + Ok(metadata) +} diff --git a/warpgate-sso/src/request.rs b/warpgate-sso/src/request.rs index 7a9c1dc9e..3f35bc431 100644 --- a/warpgate-sso/src/request.rs +++ b/warpgate-sso/src/request.rs @@ -41,67 +41,73 @@ impl SsoLoginRequest { } pub async fn verify_code(self, code: String) -> Result { - let config = self.config; + let config = self.config.clone(); let result = SsoClient::new(config.clone())? .finish_login(self.pkce_verifier, self.redirect_url, &self.nonce, code) .await?; + Ok(map_sso_result(&config, result).await) + } +} - debug!("OIDC claims: {:?}", result.claims); - debug!("OIDC userinfo claims: {:?}", result.userinfo_claims); - - macro_rules! get_claim { - ($method:ident) => { - result - .claims - .$method() - .or(result.userinfo_claims.as_ref().and_then(|x| x.$method())) - }; - } - - // If preferred_username is absent, fall back to `email` - let preferred_username = get_claim!(preferred_username) - .map(|x| x.as_str()) - .map(ToString::to_string) - .or_else(|| { - get_claim!(email) - .map(|x| x.as_str()) - .map(ToString::to_string) - }); - - let name = get_claim!(name) - .and_then(|x| x.get(None)) - .map(|x| x.as_str()) - .map(ToString::to_string); - - let email = get_claim!(email) - .map(|x| x.as_str()) - .map(ToString::to_string); - - let email_verified = get_claim!(email_verified); - - let (access_groups, admin_groups) = - match crate::google_groups::fetch_groups_if_configured(&config, email.as_deref()).await - { - Ok(Some(google_groups)) => (Some(google_groups.clone()), Some(google_groups)), - Ok(None) => ( - extract_groups(&result, config.roles_claim()), - extract_groups(&result, config.admin_roles_claim()), - ), - Err(e) => { - error!("Failed to fetch Google groups: {e}"); - (None, None) - } - }; +/// Map verified OIDC claims (+ optional userinfo claims) into a SsoLoginResponse. +/// Shared by the interactive code flow and the bearer-token (kubectl) flow. +pub async fn map_sso_result( + config: &SsoInternalProviderConfig, + result: SsoResult, +) -> SsoLoginResponse { + debug!("OIDC claims: {:?}", result.claims); + debug!("OIDC userinfo claims: {:?}", result.userinfo_claims); + + macro_rules! get_claim { + ($method:ident) => { + result + .claims + .$method() + .or(result.userinfo_claims.as_ref().and_then(|x| x.$method())) + }; + } - Ok(SsoLoginResponse { - preferred_username, - name, - email, - email_verified, - access_roles: access_groups, - admin_roles: admin_groups, - id_token: result.token.clone(), - }) + // If preferred_username is absent, fall back to `email` + let preferred_username = get_claim!(preferred_username) + .map(|x| x.as_str()) + .map(ToString::to_string) + .or_else(|| { + get_claim!(email) + .map(|x| x.as_str()) + .map(ToString::to_string) + }); + + let name = get_claim!(name) + .and_then(|x| x.get(None)) + .map(|x| x.as_str()) + .map(ToString::to_string); + + let email = get_claim!(email) + .map(|x| x.as_str()) + .map(ToString::to_string); + let email_verified = get_claim!(email_verified); + + let (access_groups, admin_groups) = + match crate::google_groups::fetch_groups_if_configured(config, email.as_deref()).await { + Ok(Some(google_groups)) => (Some(google_groups.clone()), Some(google_groups)), + Ok(None) => ( + extract_groups(&result, config.roles_claim()), + extract_groups(&result, config.admin_roles_claim()), + ), + Err(e) => { + error!("Failed to fetch Google groups: {e}"); + (None, None) + } + }; + + SsoLoginResponse { + preferred_username, + name, + email, + email_verified, + access_roles: access_groups, + admin_roles: admin_groups, + id_token: result.token.clone(), } } diff --git a/warpgate-sso/src/sso.rs b/warpgate-sso/src/sso.rs index 8429c6342..53e9ec335 100644 --- a/warpgate-sso/src/sso.rs +++ b/warpgate-sso/src/sso.rs @@ -9,18 +9,17 @@ use openidconnect::core::{ use openidconnect::url::Url; use openidconnect::{ AccessTokenHash, AdditionalClaims, Audience, AuthorizationCode, Client, CsrfToken, - DiscoveryError, EmptyExtraTokenFields, EndpointMaybeSet, EndpointNotSet, EndpointSet, - HttpClientError, IdToken, IdTokenClaims, IdTokenFields, LogoutRequest, Nonce, - OAuth2TokenResponse, PkceCodeChallenge, PkceCodeVerifier, PostLogoutRedirectUrl, - ProviderMetadataWithLogout, RedirectUrl, RequestTokenError, Scope, StandardErrorResponse, - StandardTokenResponse, TokenResponse, UserInfoClaims, reqwest, + EmptyExtraTokenFields, EndpointMaybeSet, EndpointNotSet, EndpointSet, HttpClientError, IdToken, + IdTokenClaims, IdTokenFields, LogoutRequest, Nonce, OAuth2TokenResponse, PkceCodeChallenge, + PkceCodeVerifier, PostLogoutRedirectUrl, RedirectUrl, RequestTokenError, Scope, + StandardErrorResponse, StandardTokenResponse, TokenResponse, UserInfoClaims, reqwest, }; use serde::{Deserialize, Serialize}; use tracing::error; -use crate::SsoError; use crate::config::SsoInternalProviderConfig; use crate::request::SsoLoginRequest; +use crate::{SsoError, discover_metadata}; /// A single entry in a group-style claim: either a bare string, or a /// SCIM-style object (RFC 7643) from which we take `value` (stable group ID) @@ -132,18 +131,21 @@ pub struct SsoClient { http_client: reqwest::Client, } -pub async fn discover_metadata( - config: &SsoInternalProviderConfig, - http_client: &reqwest::Client, -) -> Result { - ProviderMetadataWithLogout::discover_async(config.issuer_url()?, http_client) - .await - .map_err(|e| { - SsoError::Discovery(match e { - DiscoveryError::Request(inner) => format!("Request error: {inner:?}"), - e => format!("{e}"), - }) - }) +/// Extract the `iss` claim from a raw JWT **without** verifying its signature. +/// +/// Used only as a routing hint to pick which SSO provider should fully verify a +/// bearer token, so we can skip issuer discovery for unrelated providers. The +/// result is never trusted for any security decision. +pub fn unverified_issuer(id_token_str: &str) -> Option { + #[derive(Deserialize)] + struct IssuerClaim { + iss: Option, + } + + jsonwebtoken::dangerous::insecure_decode::(id_token_str) + .ok()? + .claims + .iss } async fn make_client( @@ -306,6 +308,74 @@ impl SsoClient { }) } + /// Verify a raw OIDC ID token (e.g. from a kubectl exec plugin) without a + /// code exchange or nonce. Validates signature against the issuer JWKS and + /// checks `iss`, `exp` and `aud` (honouring the provider's trusted-audience + /// configuration). + pub async fn verify_id_token(&self, id_token_str: &str) -> Result { + // Parse first, so a non-JWT bearer token (e.g. an API token) is rejected + // before we make any network call to the issuer. + let id_token: WarpgateIdToken = id_token_str + .parse() + .map_err(|e| SsoError::Verification(format!("Malformed ID token: {e}")))?; + + // Capture audience-check configuration before building the verifier. + let trusted: Vec = self + .config + .additional_trusted_audiences() + .cloned() + .unwrap_or_default(); + let trust_unknown = self.config.trust_unknown_audiences(); + let client_id = self.config.client_id().as_str().to_owned(); + + let client: WarpgateClient = make_client(&self.config, &self.http_client).await?; + + // Disable the built-in audience check so we can enforce it ourselves + // below. Signature / iss / exp are still fully enforced. + let token_verifier = client.id_token_verifier().require_audience_match(false); + + // No nonce in a non-interactive flow: accept any (absent) nonce. + let claims = id_token + .claims(&token_verifier, |_: Option<&Nonce>| Ok::<(), String>(()))? + .clone(); + + // Manual audience enforcement: a token is accepted iff its audience + // contains Warpgate's own client_id OR any configured trusted audience. + // When trust_unknown_audiences is true we skip the check entirely + // (documented semantics), but signature/iss/exp are always enforced. + if !trust_unknown { + let auds = claims.audiences(); + let ok = auds.iter().any(|a| { + let a = a.as_str(); + a == client_id || trusted.iter().any(|t| t == a) + }); + if !ok { + return Err(SsoError::Verification(format!( + "ID token audience not trusted (audiences: {})", + auds.iter() + .map(|a| a.as_str()) + .collect::>() + .join(", ") + ))); + } + } + + Ok(SsoResult { + token: id_token, + claims, + userinfo_claims: None, + }) + } + + /// Verify a raw ID token and map it to a SsoLoginResponse in one call. + pub async fn verify_id_token_to_response( + &self, + id_token_str: &str, + ) -> Result { + let result = self.verify_id_token(id_token_str).await?; + Ok(crate::request::map_sso_result(&self.config, result).await) + } + pub async fn logout(&self, token: WarpgateIdToken, redirect_url: Url) -> Result { let metadata = discover_metadata(&self.config, &self.http_client).await?; let Some(ref url) = metadata.additional_metadata().end_session_endpoint else { diff --git a/warpgate-web/src/admin/config/SSHKeys.svelte b/warpgate-web/src/admin/config/SSHKeys.svelte index a6193c258..3e2be2e2e 100644 --- a/warpgate-web/src/admin/config/SSHKeys.svelte +++ b/warpgate-web/src/admin/config/SSHKeys.svelte @@ -1,9 +1,9 @@ {#if targetKind === TargetKind.Ssh} - - - - - - - - - - - - - - + + + + {/if} {#if targetKind === TargetKind.Http} - - - - + {#if ticketSecret} Alternatively, set the Authorization header when accessing the URL: - - - - + {/if} {/if} {#if targetKind === TargetKind.MySql} - - - - - - - - - - - - - - + + + + Make sure you've set your client to require TLS and allowed cleartext password authentication. @@ -201,158 +193,150 @@ {/if} {#if targetKind === TargetKind.Postgres} - - - - - - - - - - - - - - - - - Make sure you've set your client to require TLS and allowed cleartext password authentication. - + + + + + + + Make sure you've set your client to require TLS and allowed cleartext password authentication. + {/if} {#if targetKind === TargetKind.Kubernetes} -

Connect with kubectl

-
-
- - {#if !ticketSecret} - {#if certificates.length > 0} - - {/if} - {#if certLoading} - - {:else} - - {#each certificates as cert (cert.credential.id)} - { - e.preventDefault() - selectCertificate(cert.credential.id) - }} - > - -
{cert.credential.label}
- {#if cert.hasLocalKey} - 0} + + {/if} + + {#if kubeconfigMode === 'oidc' && k8sOidcConfigs.length > 0} + {#if k8sOidcConfigs.length > 1} + + + {/if} + +
Requires the kubelogin (oidc-login) kubectl plugin.
+ {/if} + + {#if kubeconfigMode === 'certificate' || k8sOidcConfigs.length === 0} +
+
+ + {#if !ticketSecret} + {#if certificates.length > 0} + + {/if} + {#if certLoading} + + {:else} + + {#each certificates as cert (cert.credential.id)} + { + e.preventDefault() + selectCertificate(cert.credential.id) + }} + > + +
{cert.credential.label}
+ {#if cert.hasLocalKey} + Key available + {:else} + Key available + color="warning" + >No private key + {/if} + + {#if cert.hasLocalKey} + This certificate's private key is stored locally in this browser and can be used to generate a kubeconfig with working credentials. + {:else} + This certificate's private key is not stored locally in this browser. You can still generate a kubeconfig, but it will contain placeholders for authentication and won't work until you fill in the actual certificate and key data. + {/if} + +
+ {/each} +
+ + {#if $serverInfo?.ownCredentialManagementAllowed} + + {/if} + + {#if !selectedCertId || !clientPrivateKeyPem} + + {#if certificates.length > 0} + {#if !selectedCertId} + There is no certificate selected. {:else} - This certificate's private key is not stored locally in this browser. You can still generate a kubeconfig, but it will contain placeholders for authentication and won't work until you fill in the actual certificate and key data. + The private key for this certificate is not stored in this browser. {/if} - - - {/each} - - - {#if $serverInfo?.ownCredentialManagementAllowed} - - {/if} - - {#if !selectedCertId || !clientPrivateKeyPem} - - {#if certificates.length > 0} - {#if !selectedCertId} - There is no certificate selected. + The kubeconfig will contain placeholders for authentication. {:else} - The private key for this certificate is not stored in this browser. + You need a certificate credential to connect to this target. {/if} - The kubeconfig will contain placeholders for authentication. - {:else} - You need a certificate credential to connect to this target. - {/if} - - - You can issue a new certificate using the button above and enable the "Store in browser" option to generate a ready-to-use kubeconfig. - + + + You can issue a new certificate using the button above and enable the "Store in browser" option to generate a ready-to-use kubeconfig. + + {/if} {/if} {/if} +
+ {#if certificates.length > 0} +
+ + + + + Save the kubeconfig above to a file (e.g. warpgate-kubeconfig.yaml) and use it with kubectl. + +
{/if}
-
- - - - - - - - - - - - Save the kubeconfig above to a file (e.g. warpgate-kubeconfig.yaml) and use it with kubectl. - -
-
+ {/if} {/if} {#if issuingCertificate} - { issuingCertificate = false; loadCertificates() }} -/> + { issuingCertificate = false; loadCertificates() }} + /> {/if} {#if targetKind === TargetKind.Rdp} - - - - - - - - - + + {/if} {#if targetKind === TargetKind.Vnc} - - - - - - - - - + + {/if} diff --git a/warpgate-web/src/common/CopyableTextArea.svelte b/warpgate-web/src/common/CopyableTextArea.svelte new file mode 100644 index 000000000..4216d32d4 --- /dev/null +++ b/warpgate-web/src/common/CopyableTextArea.svelte @@ -0,0 +1,47 @@ + + + +
{value}
+ +
+ + diff --git a/warpgate-web/src/common/protocols.ts b/warpgate-web/src/common/protocols.ts index c88864892..8347a155f 100644 --- a/warpgate-web/src/common/protocols.ts +++ b/warpgate-web/src/common/protocols.ts @@ -11,6 +11,10 @@ export interface ConnectionOptions { targetDefaultDatabaseName?: string clientCertificatePem?: string clientPrivateKeyPem?: string + oidcIssuerUrl?: string + oidcClientId?: string + oidcScopes?: string[] + oidcClientSecret?: string } export function makeCommonSelectorUsername (opt: ConnectionOptions): string { @@ -194,6 +198,52 @@ users: } } +export function makeOidcKubeconfig (opt: ConnectionOptions): string { + const clusterUrl = makeKubernetesClusterUrl(opt) + const context = makeKubernetesContext(opt) + const namespace = makeKubernetesNamespace(opt) + const issuer = opt.oidcIssuerUrl ?? '' + const clientId = opt.oidcClientId ?? '' + const scopes = (opt.oidcScopes && opt.oidcScopes.length ? opt.oidcScopes : ['openid', 'email', 'profile']) + const args = [ + 'oidc-login', + 'get-token', + `--oidc-issuer-url=${issuer}`, + `--oidc-client-id=${clientId}`, + ] + if (opt.oidcClientSecret) { + args.push(`--oidc-client-secret=${opt.oidcClientSecret}`) + } + for (const s of scopes) { + args.push(`--oidc-extra-scope=${s}`) + } + const argsYaml = args.map(a => ` - ${a}`).join('\n') + return `apiVersion: v1 +kind: Config +clusters: +- cluster: + server: ${clusterUrl} + insecure-skip-tls-verify: true + name: warpgate-${opt.targetName ?? 'target'} +contexts: +- context: + cluster: warpgate-${opt.targetName ?? 'target'} + namespace: ${namespace} + user: ${context} + name: ${context} +current-context: ${context} +users: +- name: ${context} + user: + exec: + apiVersion: client.authentication.k8s.io/v1beta1 + command: kubectl + interactiveMode: IfAvailable + args: +${argsYaml} +` +} + export function makeExampleKubectlCommand (_opt: ConnectionOptions): string { return shellEscape(['kubectl', '--kubeconfig', 'warpgate-kubeconfig.yaml', 'get', 'pods']) } diff --git a/warpgate-web/src/gateway/ApiTokenManager.svelte b/warpgate-web/src/gateway/ApiTokenManager.svelte index 6efe503eb..d977e8db4 100644 --- a/warpgate-web/src/gateway/ApiTokenManager.svelte +++ b/warpgate-web/src/gateway/ApiTokenManager.svelte @@ -5,14 +5,12 @@ import { faKey } from '@fortawesome/free-solid-svg-icons' import Fa from 'svelte-fa' import CreateApiTokenModal from './CreateApiTokenModal.svelte' - import {Alert} from '@sveltestrap/sveltestrap' - import CopyButton from 'common/CopyButton.svelte' - import {Badge} from '@sveltestrap/sveltestrap' + import { Alert, Badge, Button } from '@sveltestrap/sveltestrap' import EmptyState from 'common/EmptyState.svelte' - import { Button } from '@sveltestrap/sveltestrap' import { querystring } from 'svelte-spa-router' import { get } from 'svelte/store' import { parseHumantimeDuration } from 'common/duration' + import CopyableTextArea from 'common/CopyableTextArea.svelte' let tokens: ExistingApiToken[] = $state([]) let creatingToken = $state(false) @@ -62,13 +60,7 @@ {/if} {#if lastCreatedSecret} - -
Your token - shown only once:
-
- {lastCreatedSecret} - -
-
+ {/if} diff --git a/warpgate-web/src/gateway/lib/openapi-schema.json b/warpgate-web/src/gateway/lib/openapi-schema.json index b9cc82379..3a9088bb7 100644 --- a/warpgate-web/src/gateway/lib/openapi-schema.json +++ b/warpgate-web/src/gateway/lib/openapi-schema.json @@ -2,7 +2,7 @@ "openapi": "3.0.0", "info": { "title": "Warpgate HTTP proxy", - "version": "v0.26.0-35-g86c7d219-modified" + "version": "v0.25.5-23-g57c54df3-modified" }, "servers": [ { @@ -437,6 +437,34 @@ "operationId": "initiate_sso_logout" } }, + "/sso/kubernetes-configs": { + "get": { + "responses": { + "200": { + "description": "", + "content": { + "application/json; charset=utf-8": { + "schema": { + "type": "array", + "items": { + "$ref": "#/components/schemas/SsoKubernetesConfigDescription" + } + } + } + } + } + }, + "security": [ + { + "TokenSecurityScheme": [] + }, + { + "CookieSecurityScheme": [] + } + ], + "operationId": "get_sso_kubernetes_configs" + } + }, "/sso/providers/{name}/start": { "get": { "parameters": [ @@ -2239,6 +2267,40 @@ } } }, + "SsoKubernetesConfigDescription": { + "type": "object", + "title": "SsoKubernetesConfigDescription", + "required": [ + "name", + "label", + "issuer_url", + "client_id", + "scopes" + ], + "properties": { + "name": { + "type": "string" + }, + "label": { + "type": "string" + }, + "issuer_url": { + "type": "string" + }, + "client_id": { + "type": "string" + }, + "scopes": { + "type": "array", + "items": { + "type": "string" + } + }, + "client_secret": { + "type": "string" + } + } + }, "SsoProviderDescription": { "type": "object", "title": "SsoProviderDescription", From 06323b58e69a89e3f01059574fb4796774a32330 Mon Sep 17 00:00:00 2001 From: Eugene Date: Mon, 6 Jul 2026 00:07:36 +0200 Subject: [PATCH 266/556] bump sveltestrap --- warpgate-web/package-lock.json | 73 +++++++++------------------------- 1 file changed, 18 insertions(+), 55 deletions(-) diff --git a/warpgate-web/package-lock.json b/warpgate-web/package-lock.json index 400a39664..2cea33d93 100644 --- a/warpgate-web/package-lock.json +++ b/warpgate-web/package-lock.json @@ -74,29 +74,6 @@ "url": "https://github.com/sponsors/Borewit" } }, - "node_modules/@emnapi/core": { - "version": "1.11.1", - "resolved": "https://registry.npmjs.org/@emnapi/core/-/core-1.11.1.tgz", - "integrity": "sha512-RSvbQmHzdKzNsLYa/wHrbc3KN4sYLKAdPZxqiM2HATqv/SBk2/ENSHpvXGaLOMcsAyz0poEGqkmmKYG3OWiJEQ==", - "dev": true, - "license": "MIT", - "optional": true, - "dependencies": { - "@emnapi/wasi-threads": "1.2.2", - "tslib": "^2.4.0" - } - }, - "node_modules/@emnapi/runtime": { - "version": "1.11.1", - "resolved": "https://registry.npmjs.org/@emnapi/runtime/-/runtime-1.11.1.tgz", - "integrity": "sha512-vgj7R3y3Wgx24IQaGPA/R6YFXLHVMOZ0uVEyIQPaWs+rd1AzfEMXlAC22FYwO1XkKR6NPsq7mUandH8oIRdZFw==", - "dev": true, - "license": "MIT", - "optional": true, - "dependencies": { - "tslib": "^2.4.0" - } - }, "node_modules/@emnapi/wasi-threads": { "version": "1.2.2", "resolved": "https://registry.npmjs.org/@emnapi/wasi-threads/-/wasi-threads-1.2.2.tgz", @@ -618,6 +595,7 @@ "integrity": "sha512-9zHxaDDM+oXW9As6UsP5yYB+UqczBmpeSCIFWdPEtEukMnZhxODG1BBjaUcdBB8Sc1uzojSJSJlp3yFp853t1g==", "dev": true, "license": "MIT", + "peer": true, "dependencies": { "file-type": "21.3.4", "iterare": "1.2.1", @@ -842,6 +820,7 @@ "integrity": "sha512-P1st0aksCrn9sGZhp8GMYwBnQsbvAWsZAX44oXNNvLHGqAOcoVxmjZiohstwQ7SqKnbR47akdNi+uleWD8+g6A==", "dev": true, "license": "MIT", + "peer": true, "funding": { "type": "opencollective", "url": "https://opencollective.com/popperjs" @@ -940,9 +919,6 @@ "arm64" ], "dev": true, - "libc": [ - "glibc" - ], "license": "MIT", "optional": true, "os": [ @@ -960,9 +936,6 @@ "arm64" ], "dev": true, - "libc": [ - "musl" - ], "license": "MIT", "optional": true, "os": [ @@ -980,9 +953,6 @@ "ppc64" ], "dev": true, - "libc": [ - "glibc" - ], "license": "MIT", "optional": true, "os": [ @@ -1000,9 +970,6 @@ "s390x" ], "dev": true, - "libc": [ - "glibc" - ], "license": "MIT", "optional": true, "os": [ @@ -1020,9 +987,6 @@ "x64" ], "dev": true, - "libc": [ - "glibc" - ], "license": "MIT", "optional": true, "os": [ @@ -1040,9 +1004,6 @@ "x64" ], "dev": true, - "libc": [ - "musl" - ], "license": "MIT", "optional": true, "os": [ @@ -1440,6 +1401,7 @@ "integrity": "sha512-5B7PfA2e1NQGCnDHd/0lW7W3gvp3d59Ryw54FYO8Uswxo9f6ikw3AZV+Xj/TvpImmpsiYyUqAfhC6kJID1jF6w==", "dev": true, "license": "MIT", + "peer": true, "dependencies": { "@typescript-eslint/scope-manager": "8.61.0", "@typescript-eslint/types": "8.61.0", @@ -2014,6 +1976,7 @@ "resolved": "https://registry.npmjs.org/acorn/-/acorn-8.15.0.tgz", "integrity": "sha512-NZyJarBfL7nWwIq+FDL6Zp/yHEhePMNnnJ0y3qfieCrmNvYct8uvtiV41UvlSe6apAfk0fY1FbWx+NwfmpvtTg==", "license": "MIT", + "peer": true, "bin": { "acorn": "bin/acorn" }, @@ -2303,6 +2266,7 @@ "integrity": "sha512-J8SwNxprqqpbfenehxWYXE7CW+wM1BB4w3+N+g+/Wx40xM4rsLrfPmHHxSWIxJLYDgSY/HqlFPIYb2/S3rxafw==", "dev": true, "license": "MIT", + "peer": true, "dependencies": { "follow-redirects": "^1.16.0", "form-data": "^4.0.5", @@ -3281,6 +3245,7 @@ "integrity": "sha512-LEyamqS7W5HB3ujJyvi0HQK/dtVINZvd5mAAp9eT5S/ujByGjiZLCzPcHVzuXbpJDJF/cxwHlfceVUDZ2lnSTw==", "dev": true, "license": "MIT", + "peer": true, "dependencies": { "@eslint-community/eslint-utils": "^4.8.0", "@eslint-community/regexpp": "^4.12.1", @@ -3470,6 +3435,7 @@ "integrity": "sha512-whOE1HFo/qJDyX4SnXzP4N6zOWn79WhnCUY/iDR0mPfQZO8wcYE4JClzI2oZrhBnnMUCBCHZhO6VQyoBU95mZA==", "dev": true, "license": "MIT", + "peer": true, "dependencies": { "@rtsao/scc": "^1.1.0", "array-includes": "^3.1.9", @@ -5270,9 +5236,6 @@ "arm64" ], "dev": true, - "libc": [ - "glibc" - ], "license": "MPL-2.0", "optional": true, "os": [ @@ -5294,9 +5257,6 @@ "arm64" ], "dev": true, - "libc": [ - "musl" - ], "license": "MPL-2.0", "optional": true, "os": [ @@ -5318,9 +5278,6 @@ "x64" ], "dev": true, - "libc": [ - "glibc" - ], "license": "MPL-2.0", "optional": true, "os": [ @@ -5342,9 +5299,6 @@ "x64" ], "dev": true, - "libc": [ - "musl" - ], "license": "MPL-2.0", "optional": true, "os": [ @@ -6053,6 +6007,7 @@ } ], "license": "MIT", + "peer": true, "dependencies": { "nanoid": "^3.3.12", "picocolors": "^1.1.1", @@ -6389,7 +6344,8 @@ "resolved": "https://registry.npmjs.org/quickjs-wasi/-/quickjs-wasi-2.2.0.tgz", "integrity": "sha512-zQxXmQMrEoD3S+jQdYsloq4qAuaxKFHZj6hHqOYGwB2iQZH+q9e/lf5zQPXCKOk0WJuAjzRFbO4KwHIp2D05Iw==", "dev": true, - "license": "MIT" + "license": "MIT", + "peer": true }, "node_modules/readdirp": { "version": "3.6.0", @@ -6422,7 +6378,8 @@ "resolved": "https://registry.npmjs.org/reflect-metadata/-/reflect-metadata-0.2.2.tgz", "integrity": "sha512-urBwgfrvVP/eAyXx4hluJivBKzuEbSQs9rKWCrCkbSxNv8mxPcUZKeuoF3Uy4mJl3Lwprp6yy5/39VWigZ4K6Q==", "dev": true, - "license": "Apache-2.0" + "license": "Apache-2.0", + "peer": true }, "node_modules/reflect.getprototypeof": { "version": "1.0.10", @@ -6599,6 +6556,7 @@ "integrity": "sha512-dhKf903U/PQZY6boNNtAGdWbG85WAbjT/1xYoZIC7FAY0yWapOBQVsVrDl58W86//e1VpMNBtRV4MaXfdMySFA==", "dev": true, "license": "Apache-2.0", + "peer": true, "dependencies": { "tslib": "^2.1.0" } @@ -6677,6 +6635,7 @@ "integrity": "sha512-AaIqGSrjo5lA2Yg7RvFZrlXDBCp3nV4XP73GrLGvdRWWwk+8H3l0SDvq/5bA4eF+0RFPLuWUk3E+P1U/YqnpsQ==", "dev": true, "license": "MIT", + "peer": true, "dependencies": { "chokidar": ">=3.0.0 <4.0.0", "immutable": "^4.0.0", @@ -7136,6 +7095,7 @@ "resolved": "https://registry.npmjs.org/svelte/-/svelte-5.56.3.tgz", "integrity": "sha512-w7JvrM5IFl5cmfbY0TLik9o7mjRUJmRMhOR51tBPu708Gr/MjbGs7VnJnr/B0CaXeI4vtnOh7RKxDr0cwhMdDA==", "license": "MIT", + "peer": true, "dependencies": { "@jridgewell/remapping": "^2.3.4", "@jridgewell/sourcemap-codec": "^1.5.0", @@ -7584,6 +7544,7 @@ "integrity": "sha512-jl1vZzPDinLr9eUt3J/t7V6FgNEw9QjvBPdysz9KfQDD41fQrC2Y4vKQdiaUpFT4bXlb1RHhLpp8wtm6M5TgSw==", "dev": true, "license": "Apache-2.0", + "peer": true, "bin": { "tsc": "bin/tsc", "tsserver": "bin/tsserver" @@ -7737,6 +7698,7 @@ "integrity": "sha512-EFrL7Hw4kmhZdwWO3dwwFJo6hO3FXuQ6Bg8BK/faHZ9m1YxqBS31BNSTxklIQkxK/4LlV8zTYnPsIRLBzTzjCA==", "dev": true, "hasInstallScript": true, + "peer": true, "dependencies": { "napi-postinstall": "^0.3.0" }, @@ -7788,6 +7750,7 @@ "integrity": "sha512-Ds+gBRbj0lwRO2Y5hwnUBdxSwlAve9LeRyU4sNnAr0ewW0gWF0n5bgXgUzbgZ49MV9BVUAQUFYVcDUcilUExMA==", "dev": true, "license": "MIT", + "peer": true, "dependencies": { "lightningcss": "^1.32.0", "picomatch": "^4.0.4", From 8e54b24d49ec51f144dcbd629349635cf573ff0f Mon Sep 17 00:00:00 2001 From: Eugene Date: Mon, 6 Jul 2026 00:11:49 +0200 Subject: [PATCH 267/556] bump svelte-spa-router --- warpgate-web/package-lock.json | 78 +++++++------------ warpgate-web/package.json | 3 +- warpgate-web/src/admin/config/Config.svelte | 10 ++- .../src/common/ConnectionInstructions.svelte | 2 +- .../src/gateway/ApiTokenManager.svelte | 5 +- warpgate-web/src/gateway/App.svelte | 8 +- warpgate-web/src/gateway/Login.svelte | 7 +- warpgate-web/vite.config.ts | 5 +- 8 files changed, 51 insertions(+), 67 deletions(-) diff --git a/warpgate-web/package-lock.json b/warpgate-web/package-lock.json index 2cea33d93..572c6954e 100644 --- a/warpgate-web/package-lock.json +++ b/warpgate-web/package-lock.json @@ -52,14 +52,13 @@ "svelte-intersection-observer": "^1.1.1", "svelte-observable": "^0.4.0", "svelte-preprocess": "^6.0.5", - "svelte-spa-router": "^4.0.1", + "svelte-spa-router": "^5", "thenby": "^1.4.1", "tslib": "^2.8.0", "typescript": "^5.9.3", "typescript-eslint": "^8.61.0", "ua-parser-js": "^2.0.10", "vite": "^8.1.3", - "vite-tsconfig-paths": "^6.1.1", "zmodem.js": "^0.1.10" } }, @@ -74,6 +73,29 @@ "url": "https://github.com/sponsors/Borewit" } }, + "node_modules/@emnapi/core": { + "version": "1.11.2", + "resolved": "https://registry.npmjs.org/@emnapi/core/-/core-1.11.2.tgz", + "integrity": "sha512-TC8MkTuZUtcTSiFeuC0ksCh9QIJ5+F21MvZ4Wn4ORfYaFJ/0dsiudv5tVkejgwZlwQ39jL9WWDe2lz8x0WglOA==", + "dev": true, + "license": "MIT", + "optional": true, + "dependencies": { + "@emnapi/wasi-threads": "1.2.2", + "tslib": "^2.4.0" + } + }, + "node_modules/@emnapi/runtime": { + "version": "1.11.2", + "resolved": "https://registry.npmjs.org/@emnapi/runtime/-/runtime-1.11.2.tgz", + "integrity": "sha512-kyOl3X0DuTiT1h2ft8r2fYO8JYtU9a9Xis/zBSiGArNaagCOWx90N1k2wxp18czFDH+OgcWGb5ZP/XMt3dcyPA==", + "dev": true, + "license": "MIT", + "optional": true, + "dependencies": { + "tslib": "^2.4.0" + } + }, "node_modules/@emnapi/wasi-threads": { "version": "1.2.2", "resolved": "https://registry.npmjs.org/@emnapi/wasi-threads/-/wasi-threads-1.2.2.tgz", @@ -4273,13 +4295,6 @@ "url": "https://github.com/sponsors/ljharb" } }, - "node_modules/globrex": { - "version": "0.1.2", - "resolved": "https://registry.npmjs.org/globrex/-/globrex-0.1.2.tgz", - "integrity": "sha512-uHJgbwAMwNFf5mLst7IWLNg14x1CkeqglJb/K3doi4dw6q2IvAAmM/Y81kevy83wP+Sst+nutFTYOGg3d1lsxg==", - "dev": true, - "license": "MIT" - }, "node_modules/gopd": { "version": "1.2.0", "resolved": "https://registry.npmjs.org/gopd/-/gopd-1.2.0.tgz", @@ -7288,9 +7303,9 @@ } }, "node_modules/svelte-spa-router": { - "version": "4.0.1", - "resolved": "https://registry.npmjs.org/svelte-spa-router/-/svelte-spa-router-4.0.1.tgz", - "integrity": "sha512-2JkmUQ2f9jRluijL58LtdQBIpynSbem2eBGp4zXdi7aDY1znbR6yjw0KsonD0aq2QLwf4Yx4tBJQjxIjgjXHKg==", + "version": "5.1.1", + "resolved": "https://registry.npmjs.org/svelte-spa-router/-/svelte-spa-router-5.1.1.tgz", + "integrity": "sha512-/9PiiyS2jQIXlP+NlKRcPl/Bgwm6WTCTmJq8TZw5IShEbozwcfpX2V3YEL4lAVgJJ2Ni+ipAH7aYVEupeNwtHA==", "dev": true, "license": "MIT", "dependencies": { @@ -7298,6 +7313,9 @@ }, "funding": { "url": "https://github.com/sponsors/ItalyPaleAle" + }, + "peerDependencies": { + "svelte": "^5.0.0" } }, "node_modules/thenby": { @@ -7393,27 +7411,6 @@ "typescript": ">=4.8.4" } }, - "node_modules/tsconfck": { - "version": "3.1.5", - "resolved": "https://registry.npmjs.org/tsconfck/-/tsconfck-3.1.5.tgz", - "integrity": "sha512-CLDfGgUp7XPswWnezWwsCRxNmgQjhYq3VXHM0/XIRxhVrKw0M1if9agzryh1QS3nxjCROvV+xWxoJO1YctzzWg==", - "dev": true, - "license": "MIT", - "bin": { - "tsconfck": "bin/tsconfck.js" - }, - "engines": { - "node": "^18 || >=20" - }, - "peerDependencies": { - "typescript": "^5.0.0" - }, - "peerDependenciesMeta": { - "typescript": { - "optional": true - } - } - }, "node_modules/tsconfig-paths": { "version": "3.15.0", "resolved": "https://registry.npmjs.org/tsconfig-paths/-/tsconfig-paths-3.15.0.tgz", @@ -7823,21 +7820,6 @@ } } }, - "node_modules/vite-tsconfig-paths": { - "version": "6.1.1", - "resolved": "https://registry.npmjs.org/vite-tsconfig-paths/-/vite-tsconfig-paths-6.1.1.tgz", - "integrity": "sha512-2cihq7zliibCCZ8P9cKJrQBkfgdvcFkOOc3Y02o3GWUDLgqjWsZudaoiuOwO/gzTzy17cS5F7ZPo4bsnS4DGkg==", - "dev": true, - "license": "MIT", - "dependencies": { - "debug": "^4.1.1", - "globrex": "^0.1.2", - "tsconfck": "^3.0.3" - }, - "peerDependencies": { - "vite": "*" - } - }, "node_modules/vitefu": { "version": "1.1.3", "resolved": "https://registry.npmjs.org/vitefu/-/vitefu-1.1.3.tgz", diff --git a/warpgate-web/package.json b/warpgate-web/package.json index 4b7b148fb..2fcf265cc 100644 --- a/warpgate-web/package.json +++ b/warpgate-web/package.json @@ -59,14 +59,13 @@ "svelte-intersection-observer": "^1.1.1", "svelte-observable": "^0.4.0", "svelte-preprocess": "^6.0.5", - "svelte-spa-router": "^4.0.1", + "svelte-spa-router": "^5", "thenby": "^1.4.1", "tslib": "^2.8.0", "typescript": "^5.9.3", "typescript-eslint": "^8.61.0", "ua-parser-js": "^2.0.10", "vite": "^8.1.3", - "vite-tsconfig-paths": "^6.1.1", "zmodem.js": "^0.1.10" }, "overrides": { diff --git a/warpgate-web/src/admin/config/Config.svelte b/warpgate-web/src/admin/config/Config.svelte index f33aefae6..5dbbcd79f 100644 --- a/warpgate-web/src/admin/config/Config.svelte +++ b/warpgate-web/src/admin/config/Config.svelte @@ -1,7 +1,7 @@ {#snippet navItems()} @@ -176,9 +180,7 @@
- { - sidebarMode = e.detail.route !== '' - }} /> +
diff --git a/warpgate-web/src/common/ConnectionInstructions.svelte b/warpgate-web/src/common/ConnectionInstructions.svelte index 1f074c9fe..58679aa27 100644 --- a/warpgate-web/src/common/ConnectionInstructions.svelte +++ b/warpgate-web/src/common/ConnectionInstructions.svelte @@ -1,5 +1,5 @@ @@ -151,9 +155,7 @@ {/if}
- { - doNotShowAuthRequests = !!(e.detail.userData as any)?.['doNotShowAuthRequests'] - }} /> +