mirror of
https://github.com/AvengeMedia/DankMaterialShell.git
synced 2026-06-24 03:55:23 -04:00
fix(network): fix excessive network prompting on failures, add VPN
connection statuses
This commit is contained in:
@@ -361,11 +361,27 @@ func (a *SecretAgent) GetSecrets(
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// Phase 4: Non-interactive secret retrieval (keyring).
|
// Phase 4: Non-interactive secret retrieval. REQUEST_NEW means NM thinks the
|
||||||
// Always try the keyring even when REQUEST_NEW is set — the vault may have
|
// secret is wrong, so force a prompt; otherwise reuse keyring then cached secret.
|
||||||
// been unlocked by a prior call's Prompt flow, making the lookup non-interactive.
|
requestNew := flags&nmSecretAgentFlagRequestNew != 0
|
||||||
if secretOut := a.trySecretService(connUuid, settingName, fields); secretOut != nil {
|
if requestNew {
|
||||||
return secretOut, nil
|
if a.backend != nil {
|
||||||
|
a.backend.clearCachedWiFiSecret(connUuid)
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
if secretOut := a.trySecretService(connUuid, settingName, fields); secretOut != nil {
|
||||||
|
return secretOut, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
switch settingName {
|
||||||
|
case "802-11-wireless-security", "802-1x":
|
||||||
|
if a.backend != nil {
|
||||||
|
if cached := a.backend.lookupCachedWiFiSecret(connUuid, settingName); cached != nil {
|
||||||
|
log.Infof("[SecretAgent] Reusing cached WiFi secret for %s (no REQUEST_NEW)", connUuid)
|
||||||
|
return buildWiFiSecretsResponse(settingName, cached), nil
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// Phase 5: If interaction is not allowed, we're done.
|
// Phase 5: If interaction is not allowed, we're done.
|
||||||
@@ -436,6 +452,8 @@ func (a *SecretAgent) GetSecrets(
|
|||||||
}
|
}
|
||||||
a.backend.stateMutex.Unlock()
|
a.backend.stateMutex.Unlock()
|
||||||
|
|
||||||
|
a.backend.clearCachedWiFiSecret(connUuid)
|
||||||
|
|
||||||
// If this was a WiFi connection that was just cancelled, remove the connection profile
|
// If this was a WiFi connection that was just cancelled, remove the connection profile
|
||||||
// (it was created with AddConnection but activation was cancelled)
|
// (it was created with AddConnection but activation was cancelled)
|
||||||
// Only do this for newly created connections, not pre-existing ones.
|
// Only do this for newly created connections, not pre-existing ones.
|
||||||
@@ -539,6 +557,13 @@ func (a *SecretAgent) GetSecrets(
|
|||||||
log.Infof("[SecretAgent] Queued credentials persist for after connection succeeds")
|
log.Infof("[SecretAgent] Queued credentials persist for after connection succeeds")
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if a.backend != nil {
|
||||||
|
switch settingName {
|
||||||
|
case "802-11-wireless-security", "802-1x":
|
||||||
|
a.backend.cacheWiFiSecret(connUuid, ssid, settingName, reply.Secrets)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
return out, nil
|
return out, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -906,6 +931,27 @@ func reasonFromFlags(flags uint32) string {
|
|||||||
return "required"
|
return "required"
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func buildWiFiSecretsResponse(settingName string, secrets map[string]string) nmSettingMap {
|
||||||
|
sec := nmVariantMap{}
|
||||||
|
switch settingName {
|
||||||
|
case "802-1x":
|
||||||
|
for k, v := range secrets {
|
||||||
|
switch k {
|
||||||
|
case "password", "private-key-password", "phase2-private-key-password", "pin":
|
||||||
|
sec[k] = dbus.MakeVariant(v)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
default:
|
||||||
|
for k, v := range secrets {
|
||||||
|
sec[k] = dbus.MakeVariant(v)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
out := nmSettingMap{}
|
||||||
|
out[settingName] = sec
|
||||||
|
return out
|
||||||
|
}
|
||||||
|
|
||||||
func buildGPSamlSecretsResponse(settingName, cookie, host, fingerprint string) nmSettingMap {
|
func buildGPSamlSecretsResponse(settingName, cookie, host, fingerprint string) nmSettingMap {
|
||||||
out := nmSettingMap{}
|
out := nmSettingMap{}
|
||||||
vpnSec := nmVariantMap{}
|
vpnSec := nmVariantMap{}
|
||||||
|
|||||||
@@ -374,6 +374,57 @@ func TestSecretAgent_GetSecrets_NoInteractionFlag(t *testing.T) {
|
|||||||
assert.Contains(t, err.Error(), "NoSecrets")
|
assert.Contains(t, err.Error(), "NoSecrets")
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func TestBuildWiFiSecretsResponse(t *testing.T) {
|
||||||
|
t.Run("wpa-psk returns psk", func(t *testing.T) {
|
||||||
|
out := buildWiFiSecretsResponse("802-11-wireless-security", map[string]string{"psk": "hunter2"})
|
||||||
|
|
||||||
|
sec, ok := out["802-11-wireless-security"]
|
||||||
|
assert.True(t, ok)
|
||||||
|
assert.Equal(t, "hunter2", sec["psk"].Value())
|
||||||
|
})
|
||||||
|
|
||||||
|
t.Run("802-1x keeps secrets and drops identity", func(t *testing.T) {
|
||||||
|
out := buildWiFiSecretsResponse("802-1x", map[string]string{
|
||||||
|
"identity": "john",
|
||||||
|
"password": "hunter2",
|
||||||
|
})
|
||||||
|
|
||||||
|
sec := out["802-1x"]
|
||||||
|
assert.Equal(t, "hunter2", sec["password"].Value())
|
||||||
|
_, hasIdentity := sec["identity"]
|
||||||
|
assert.False(t, hasIdentity, "identity is persisted separately, not returned as a secret")
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestWiFiSecretCache(t *testing.T) {
|
||||||
|
b := &NetworkManagerBackend{}
|
||||||
|
|
||||||
|
b.cacheWiFiSecret("uuid-1", "HomeNet", "802-11-wireless-security", map[string]string{"psk": "hunter2"})
|
||||||
|
|
||||||
|
got := b.lookupCachedWiFiSecret("uuid-1", "802-11-wireless-security")
|
||||||
|
assert.Equal(t, map[string]string{"psk": "hunter2"}, got)
|
||||||
|
|
||||||
|
assert.Nil(t, b.lookupCachedWiFiSecret("uuid-1", "802-1x"), "setting mismatch must miss")
|
||||||
|
assert.Nil(t, b.lookupCachedWiFiSecret("uuid-2", "802-11-wireless-security"), "uuid mismatch must miss")
|
||||||
|
assert.Nil(t, b.lookupCachedWiFiSecret("", "802-11-wireless-security"), "empty uuid must miss")
|
||||||
|
|
||||||
|
// REQUEST_NEW path clears by uuid.
|
||||||
|
b.clearCachedWiFiSecret("uuid-1")
|
||||||
|
assert.Nil(t, b.lookupCachedWiFiSecret("uuid-1", "802-11-wireless-security"))
|
||||||
|
|
||||||
|
// Returned map is a copy: mutating it must not affect the cache.
|
||||||
|
b.cacheWiFiSecret("uuid-1", "HomeNet", "802-11-wireless-security", map[string]string{"psk": "hunter2"})
|
||||||
|
got = b.lookupCachedWiFiSecret("uuid-1", "802-11-wireless-security")
|
||||||
|
got["psk"] = "tampered"
|
||||||
|
assert.Equal(t, "hunter2", b.lookupCachedWiFiSecret("uuid-1", "802-11-wireless-security")["psk"])
|
||||||
|
|
||||||
|
// Terminal-state path clears by SSID.
|
||||||
|
b.clearCachedWiFiSecretBySSID("OtherNet")
|
||||||
|
assert.NotNil(t, b.lookupCachedWiFiSecret("uuid-1", "802-11-wireless-security"), "ssid mismatch must not clear")
|
||||||
|
b.clearCachedWiFiSecretBySSID("HomeNet")
|
||||||
|
assert.Nil(t, b.lookupCachedWiFiSecret("uuid-1", "802-11-wireless-security"))
|
||||||
|
}
|
||||||
|
|
||||||
func TestNmVariantMap(t *testing.T) {
|
func TestNmVariantMap(t *testing.T) {
|
||||||
// Test that nmVariantMap and nmSettingMap work correctly
|
// Test that nmVariantMap and nmSettingMap work correctly
|
||||||
settingMap := make(nmSettingMap)
|
settingMap := make(nmSettingMap)
|
||||||
|
|||||||
@@ -79,4 +79,6 @@ type BackendState struct {
|
|||||||
IsConnectingVPN bool
|
IsConnectingVPN bool
|
||||||
ConnectingVPNUUID string
|
ConnectingVPNUUID string
|
||||||
LastError string
|
LastError string
|
||||||
|
VPNError string
|
||||||
|
VPNErrorUuid string
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -16,6 +16,9 @@ const (
|
|||||||
dbusNMWiredInterface = "org.freedesktop.NetworkManager.Device.Wired"
|
dbusNMWiredInterface = "org.freedesktop.NetworkManager.Device.Wired"
|
||||||
dbusNMWirelessInterface = "org.freedesktop.NetworkManager.Device.Wireless"
|
dbusNMWirelessInterface = "org.freedesktop.NetworkManager.Device.Wireless"
|
||||||
dbusNMAccessPointInterface = "org.freedesktop.NetworkManager.AccessPoint"
|
dbusNMAccessPointInterface = "org.freedesktop.NetworkManager.AccessPoint"
|
||||||
|
dbusNMActiveConnInterface = "org.freedesktop.NetworkManager.Connection.Active"
|
||||||
|
dbusNMVPNConnInterface = "org.freedesktop.NetworkManager.VPN.Connection"
|
||||||
|
dbusNMActiveConnPath = "/org/freedesktop/NetworkManager/ActiveConnection"
|
||||||
dbusPropsInterface = "org.freedesktop.DBus.Properties"
|
dbusPropsInterface = "org.freedesktop.DBus.Properties"
|
||||||
|
|
||||||
NmDeviceStateReasonWrongPassword = 8
|
NmDeviceStateReasonWrongPassword = 8
|
||||||
@@ -77,6 +80,8 @@ type NetworkManagerBackend struct {
|
|||||||
cachedPKCS11Mu sync.Mutex
|
cachedPKCS11Mu sync.Mutex
|
||||||
cachedGPSamlCookie *cachedGPSamlCookie
|
cachedGPSamlCookie *cachedGPSamlCookie
|
||||||
cachedGPSamlMu sync.Mutex
|
cachedGPSamlMu sync.Mutex
|
||||||
|
cachedWiFiSecret *cachedWiFiSecret
|
||||||
|
cachedWiFiSecretMu sync.Mutex
|
||||||
|
|
||||||
onStateChange func()
|
onStateChange func()
|
||||||
}
|
}
|
||||||
@@ -99,6 +104,15 @@ type cachedPKCS11PIN struct {
|
|||||||
PIN string
|
PIN string
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// cachedWiFiSecret reuses a just-entered WiFi/802.1x secret across repeat
|
||||||
|
// GetSecrets calls in one activation, so NM retries don't re-prompt.
|
||||||
|
type cachedWiFiSecret struct {
|
||||||
|
ConnectionUUID string
|
||||||
|
SSID string
|
||||||
|
SettingName string
|
||||||
|
Secrets map[string]string
|
||||||
|
}
|
||||||
|
|
||||||
type cachedGPSamlCookie struct {
|
type cachedGPSamlCookie struct {
|
||||||
ConnectionUUID string
|
ConnectionUUID string
|
||||||
Cookie string
|
Cookie string
|
||||||
@@ -340,6 +354,104 @@ func (b *NetworkManagerBackend) CancelCredentials(token string) error {
|
|||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// mergeStoredSecrets re-fetches stored secrets and folds them into settings
|
||||||
|
// before an Update. GetSettings never returns secrets and Update replaces the
|
||||||
|
// whole connection, so a bare GetSettings->Update wipes system-owned passwords
|
||||||
|
// (e.g. an OpenVPN password with password-flags=0). Only fills keys that aren't
|
||||||
|
// already being set, so an explicit credential change still wins.
|
||||||
|
func mergeStoredSecrets(conn gonetworkmanager.Connection, settings gonetworkmanager.ConnectionSettings) {
|
||||||
|
for setting := range settings {
|
||||||
|
switch setting {
|
||||||
|
case "vpn", "802-11-wireless-security", "802-1x":
|
||||||
|
default:
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
|
||||||
|
secrets, err := conn.GetSecrets(setting)
|
||||||
|
if err != nil {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
|
||||||
|
section, ok := secrets[setting]
|
||||||
|
if !ok {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
|
||||||
|
for k, v := range section {
|
||||||
|
if _, exists := settings[setting][k]; exists {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
settings[setting][k] = v
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (b *NetworkManagerBackend) cacheWiFiSecret(connUUID, ssid, settingName string, secrets map[string]string) {
|
||||||
|
if connUUID == "" || len(secrets) == 0 {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
copied := make(map[string]string, len(secrets))
|
||||||
|
for k, v := range secrets {
|
||||||
|
copied[k] = v
|
||||||
|
}
|
||||||
|
|
||||||
|
b.cachedWiFiSecretMu.Lock()
|
||||||
|
b.cachedWiFiSecret = &cachedWiFiSecret{
|
||||||
|
ConnectionUUID: connUUID,
|
||||||
|
SSID: ssid,
|
||||||
|
SettingName: settingName,
|
||||||
|
Secrets: copied,
|
||||||
|
}
|
||||||
|
b.cachedWiFiSecretMu.Unlock()
|
||||||
|
}
|
||||||
|
|
||||||
|
func (b *NetworkManagerBackend) lookupCachedWiFiSecret(connUUID, settingName string) map[string]string {
|
||||||
|
if connUUID == "" {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
b.cachedWiFiSecretMu.Lock()
|
||||||
|
defer b.cachedWiFiSecretMu.Unlock()
|
||||||
|
|
||||||
|
cached := b.cachedWiFiSecret
|
||||||
|
if cached == nil || cached.ConnectionUUID != connUUID || cached.SettingName != settingName {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
copied := make(map[string]string, len(cached.Secrets))
|
||||||
|
for k, v := range cached.Secrets {
|
||||||
|
copied[k] = v
|
||||||
|
}
|
||||||
|
return copied
|
||||||
|
}
|
||||||
|
|
||||||
|
func (b *NetworkManagerBackend) clearCachedWiFiSecret(connUUID string) {
|
||||||
|
b.cachedWiFiSecretMu.Lock()
|
||||||
|
defer b.cachedWiFiSecretMu.Unlock()
|
||||||
|
|
||||||
|
if connUUID == "" {
|
||||||
|
b.cachedWiFiSecret = nil
|
||||||
|
return
|
||||||
|
}
|
||||||
|
if b.cachedWiFiSecret != nil && b.cachedWiFiSecret.ConnectionUUID == connUUID {
|
||||||
|
b.cachedWiFiSecret = nil
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (b *NetworkManagerBackend) clearCachedWiFiSecretBySSID(ssid string) {
|
||||||
|
if ssid == "" {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
b.cachedWiFiSecretMu.Lock()
|
||||||
|
defer b.cachedWiFiSecretMu.Unlock()
|
||||||
|
|
||||||
|
if b.cachedWiFiSecret != nil && b.cachedWiFiSecret.SSID == ssid {
|
||||||
|
b.cachedWiFiSecret = nil
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
func (b *NetworkManagerBackend) ensureWiFiDevice() error {
|
func (b *NetworkManagerBackend) ensureWiFiDevice() error {
|
||||||
if b.wifiDev != nil {
|
if b.wifiDev != nil {
|
||||||
return nil
|
return nil
|
||||||
|
|||||||
@@ -136,6 +136,29 @@ func (b *NetworkManagerBackend) startSignalPump() error {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// activating->activated/failed fires on the active-connection object, not the
|
||||||
|
// manager's ActiveConnections property. VPN.Connection covers plugin VPNs;
|
||||||
|
// Connection.Active covers the rest, including WireGuard.
|
||||||
|
if err := conn.AddMatchSignal(
|
||||||
|
dbus.WithMatchPathNamespace(dbus.ObjectPath(dbusNMActiveConnPath)),
|
||||||
|
dbus.WithMatchInterface(dbusNMVPNConnInterface),
|
||||||
|
dbus.WithMatchMember("VpnStateChanged"),
|
||||||
|
); err != nil {
|
||||||
|
conn.RemoveSignal(signals)
|
||||||
|
conn.Close()
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
if err := conn.AddMatchSignal(
|
||||||
|
dbus.WithMatchPathNamespace(dbus.ObjectPath(dbusNMActiveConnPath)),
|
||||||
|
dbus.WithMatchInterface(dbusNMActiveConnInterface),
|
||||||
|
dbus.WithMatchMember("StateChanged"),
|
||||||
|
); err != nil {
|
||||||
|
conn.RemoveSignal(signals)
|
||||||
|
conn.Close()
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
b.sigWG.Add(1)
|
b.sigWG.Add(1)
|
||||||
go func() {
|
go func() {
|
||||||
defer b.sigWG.Done()
|
defer b.sigWG.Done()
|
||||||
@@ -193,6 +216,16 @@ func (b *NetworkManagerBackend) stopSignalPump() {
|
|||||||
dbus.WithMatchInterface(dbusNMInterface),
|
dbus.WithMatchInterface(dbusNMInterface),
|
||||||
dbus.WithMatchMember("DeviceRemoved"),
|
dbus.WithMatchMember("DeviceRemoved"),
|
||||||
)
|
)
|
||||||
|
b.dbusConn.RemoveMatchSignal(
|
||||||
|
dbus.WithMatchPathNamespace(dbus.ObjectPath(dbusNMActiveConnPath)),
|
||||||
|
dbus.WithMatchInterface(dbusNMVPNConnInterface),
|
||||||
|
dbus.WithMatchMember("VpnStateChanged"),
|
||||||
|
)
|
||||||
|
b.dbusConn.RemoveMatchSignal(
|
||||||
|
dbus.WithMatchPathNamespace(dbus.ObjectPath(dbusNMActiveConnPath)),
|
||||||
|
dbus.WithMatchInterface(dbusNMActiveConnInterface),
|
||||||
|
dbus.WithMatchMember("StateChanged"),
|
||||||
|
)
|
||||||
|
|
||||||
for _, info := range b.wifiDevices {
|
for _, info := range b.wifiDevices {
|
||||||
b.dbusConn.RemoveMatchSignal(
|
b.dbusConn.RemoveMatchSignal(
|
||||||
@@ -234,6 +267,20 @@ func (b *NetworkManagerBackend) handleDBusSignal(sig *dbus.Signal) {
|
|||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if sig.Name == dbusNMVPNConnInterface+".VpnStateChanged" {
|
||||||
|
if len(sig.Body) >= 2 {
|
||||||
|
state, _ := sig.Body[0].(uint32)
|
||||||
|
reason, _ := sig.Body[1].(uint32)
|
||||||
|
b.handleVPNStateChange(state, reason)
|
||||||
|
}
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
if sig.Name == dbusNMActiveConnInterface+".StateChanged" {
|
||||||
|
b.handleActiveConnectionStateChange()
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
if sig.Name == "org.freedesktop.NetworkManager.DeviceAdded" {
|
if sig.Name == "org.freedesktop.NetworkManager.DeviceAdded" {
|
||||||
if len(sig.Body) >= 1 {
|
if len(sig.Body) >= 1 {
|
||||||
if devicePath, ok := sig.Body[0].(dbus.ObjectPath); ok {
|
if devicePath, ok := sig.Body[0].(dbus.ObjectPath); ok {
|
||||||
@@ -320,6 +367,27 @@ func (b *NetworkManagerBackend) handleNetworkManagerChange(changes map[string]db
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func (b *NetworkManagerBackend) handleActiveConnectionStateChange() {
|
||||||
|
b.updateVPNConnectionState()
|
||||||
|
b.ListActiveVPN()
|
||||||
|
if b.onStateChange != nil {
|
||||||
|
b.onStateChange()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (b *NetworkManagerBackend) handleVPNStateChange(state, reason uint32) {
|
||||||
|
if state == nmVPNStateFailed {
|
||||||
|
b.stateMutex.Lock()
|
||||||
|
if uuid := b.state.ConnectingVPNUUID; uuid != "" {
|
||||||
|
b.state.VPNError = vpnFailureMessage(reason)
|
||||||
|
b.state.VPNErrorUuid = uuid
|
||||||
|
}
|
||||||
|
b.stateMutex.Unlock()
|
||||||
|
}
|
||||||
|
|
||||||
|
b.handleActiveConnectionStateChange()
|
||||||
|
}
|
||||||
|
|
||||||
func (b *NetworkManagerBackend) handleDeviceChange(devicePath dbus.ObjectPath, changes map[string]dbus.Variant) {
|
func (b *NetworkManagerBackend) handleDeviceChange(devicePath dbus.ObjectPath, changes map[string]dbus.Variant) {
|
||||||
var needsUpdate bool
|
var needsUpdate bool
|
||||||
var stateChanged bool
|
var stateChanged bool
|
||||||
|
|||||||
@@ -213,6 +213,7 @@ func (b *NetworkManagerBackend) updateWiFiState() error {
|
|||||||
}
|
}
|
||||||
|
|
||||||
var forgetSSID string
|
var forgetSSID string
|
||||||
|
var doneSSID string
|
||||||
|
|
||||||
b.stateMutex.Lock()
|
b.stateMutex.Lock()
|
||||||
|
|
||||||
@@ -226,6 +227,7 @@ func (b *NetworkManagerBackend) updateWiFiState() error {
|
|||||||
b.state.IsConnecting = false
|
b.state.IsConnecting = false
|
||||||
b.state.ConnectingSSID = ""
|
b.state.ConnectingSSID = ""
|
||||||
b.state.LastError = ""
|
b.state.LastError = ""
|
||||||
|
doneSSID = connectingSSID
|
||||||
case failed || (disconnected && !connected):
|
case failed || (disconnected && !connected):
|
||||||
log.Warnf("[updateWiFiState] Connection failed: SSID=%s, state=%d", connectingSSID, state)
|
log.Warnf("[updateWiFiState] Connection failed: SSID=%s, state=%d", connectingSSID, state)
|
||||||
b.state.IsConnecting = false
|
b.state.IsConnecting = false
|
||||||
@@ -240,6 +242,8 @@ func (b *NetworkManagerBackend) updateWiFiState() error {
|
|||||||
b.lastFailedSSID = connectingSSID
|
b.lastFailedSSID = connectingSSID
|
||||||
b.lastFailedTime = time.Now().Unix()
|
b.lastFailedTime = time.Now().Unix()
|
||||||
b.failedMutex.Unlock()
|
b.failedMutex.Unlock()
|
||||||
|
|
||||||
|
doneSSID = connectingSSID
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -252,6 +256,10 @@ func (b *NetworkManagerBackend) updateWiFiState() error {
|
|||||||
|
|
||||||
b.stateMutex.Unlock()
|
b.stateMutex.Unlock()
|
||||||
|
|
||||||
|
if doneSSID != "" {
|
||||||
|
b.clearCachedWiFiSecretBySSID(doneSSID)
|
||||||
|
}
|
||||||
|
|
||||||
if forgetSSID != "" {
|
if forgetSSID != "" {
|
||||||
log.Infof("[updateWiFiState] User cancelled authentication, removing connection profile for %s", forgetSSID)
|
log.Infof("[updateWiFiState] User cancelled authentication, removing connection profile for %s", forgetSSID)
|
||||||
if err := b.ForgetWiFiNetwork(forgetSSID); err != nil {
|
if err := b.ForgetWiFiNetwork(forgetSSID); err != nil {
|
||||||
|
|||||||
@@ -17,6 +17,24 @@ import (
|
|||||||
"github.com/godbus/dbus/v5"
|
"github.com/godbus/dbus/v5"
|
||||||
)
|
)
|
||||||
|
|
||||||
|
const nmVPNStateFailed = 6
|
||||||
|
|
||||||
|
// vpnFailureMessage maps NMVpnConnectionStateReason to a user-facing message.
|
||||||
|
func vpnFailureMessage(reason uint32) string {
|
||||||
|
switch reason {
|
||||||
|
case 9: // NO_SECRETS
|
||||||
|
return "Authentication required"
|
||||||
|
case 10: // LOGIN_FAILED
|
||||||
|
return "Authentication failed"
|
||||||
|
case 6: // CONNECT_TIMEOUT
|
||||||
|
return "Connection timed out"
|
||||||
|
case 7, 8: // SERVICE_START_TIMEOUT, SERVICE_START_FAILED
|
||||||
|
return "VPN service failed to start"
|
||||||
|
default:
|
||||||
|
return "VPN connection failed"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
func (b *NetworkManagerBackend) ListVPNProfiles() ([]VPNProfile, error) {
|
func (b *NetworkManagerBackend) ListVPNProfiles() ([]VPNProfile, error) {
|
||||||
s := b.settings
|
s := b.settings
|
||||||
if s == nil {
|
if s == nil {
|
||||||
@@ -199,25 +217,38 @@ func (b *NetworkManagerBackend) ListActiveVPN() ([]VPNActive, error) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
func (b *NetworkManagerBackend) ConnectVPN(uuidOrName string, singleActive bool) error {
|
func (b *NetworkManagerBackend) ConnectVPN(uuidOrName string, singleActive bool) error {
|
||||||
|
// Drop any stale connecting state from a prior attempt that never resolved;
|
||||||
|
// a leftover flag makes the secret agent refuse secrets for other VPNs.
|
||||||
|
b.stateMutex.Lock()
|
||||||
|
b.state.IsConnectingVPN = false
|
||||||
|
b.state.ConnectingVPNUUID = ""
|
||||||
|
b.state.VPNError = ""
|
||||||
|
b.state.VPNErrorUuid = ""
|
||||||
|
b.stateMutex.Unlock()
|
||||||
|
|
||||||
if singleActive {
|
if singleActive {
|
||||||
active, err := b.ListActiveVPN()
|
active, err := b.ListActiveVPN()
|
||||||
if err == nil && len(active) > 0 {
|
if err == nil && len(active) > 0 {
|
||||||
alreadyConnected := false
|
alreadyConnected := false
|
||||||
for _, vpn := range active {
|
for _, vpn := range active {
|
||||||
if vpn.UUID == uuidOrName || vpn.Name == uuidOrName {
|
if vpn.UUID != uuidOrName && vpn.Name != uuidOrName {
|
||||||
alreadyConnected = true
|
continue
|
||||||
break
|
|
||||||
}
|
}
|
||||||
|
switch vpn.State {
|
||||||
|
case "activated", "activating":
|
||||||
|
alreadyConnected = true
|
||||||
|
}
|
||||||
|
break
|
||||||
}
|
}
|
||||||
|
|
||||||
if !alreadyConnected {
|
if alreadyConnected {
|
||||||
if err := b.DisconnectAllVPN(); err != nil {
|
|
||||||
log.Warnf("Failed to disconnect existing VPNs: %v", err)
|
|
||||||
}
|
|
||||||
time.Sleep(500 * time.Millisecond)
|
|
||||||
} else {
|
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if err := b.DisconnectAllVPN(); err != nil {
|
||||||
|
log.Warnf("Failed to disconnect existing VPNs: %v", err)
|
||||||
|
}
|
||||||
|
time.Sleep(500 * time.Millisecond)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -723,6 +754,8 @@ func (b *NetworkManagerBackend) updateVPNConnectionState() {
|
|||||||
b.state.IsConnectingVPN = false
|
b.state.IsConnectingVPN = false
|
||||||
b.state.ConnectingVPNUUID = ""
|
b.state.ConnectingVPNUUID = ""
|
||||||
b.state.LastError = ""
|
b.state.LastError = ""
|
||||||
|
b.state.VPNError = ""
|
||||||
|
b.state.VPNErrorUuid = ""
|
||||||
b.stateMutex.Unlock()
|
b.stateMutex.Unlock()
|
||||||
|
|
||||||
// Clear cached PKCS11 PIN and SAML cookie on success
|
// Clear cached PKCS11 PIN and SAML cookie on success
|
||||||
@@ -748,6 +781,10 @@ func (b *NetworkManagerBackend) updateVPNConnectionState() {
|
|||||||
b.state.IsConnectingVPN = false
|
b.state.IsConnectingVPN = false
|
||||||
b.state.ConnectingVPNUUID = ""
|
b.state.ConnectingVPNUUID = ""
|
||||||
b.state.LastError = "VPN connection failed"
|
b.state.LastError = "VPN connection failed"
|
||||||
|
if b.state.VPNError == "" {
|
||||||
|
b.state.VPNError = "VPN connection failed"
|
||||||
|
}
|
||||||
|
b.state.VPNErrorUuid = connectingVPNUUID
|
||||||
b.stateMutex.Unlock()
|
b.stateMutex.Unlock()
|
||||||
|
|
||||||
// Clear cached PKCS11 PIN and SAML cookie on failure
|
// Clear cached PKCS11 PIN and SAML cookie on failure
|
||||||
@@ -768,6 +805,10 @@ func (b *NetworkManagerBackend) updateVPNConnectionState() {
|
|||||||
b.state.IsConnectingVPN = false
|
b.state.IsConnectingVPN = false
|
||||||
b.state.ConnectingVPNUUID = ""
|
b.state.ConnectingVPNUUID = ""
|
||||||
b.state.LastError = "VPN connection failed"
|
b.state.LastError = "VPN connection failed"
|
||||||
|
if b.state.VPNError == "" {
|
||||||
|
b.state.VPNError = "VPN connection failed"
|
||||||
|
}
|
||||||
|
b.state.VPNErrorUuid = connectingVPNUUID
|
||||||
b.stateMutex.Unlock()
|
b.stateMutex.Unlock()
|
||||||
|
|
||||||
// Clear cached PKCS11 PIN and SAML cookie
|
// Clear cached PKCS11 PIN and SAML cookie
|
||||||
@@ -1217,6 +1258,8 @@ func (b *NetworkManagerBackend) UpdateVPNConfig(connUUID string, updates map[str
|
|||||||
delete(ipv6, "dns")
|
delete(ipv6, "dns")
|
||||||
}
|
}
|
||||||
|
|
||||||
|
mergeStoredSecrets(conn, settings)
|
||||||
|
|
||||||
if err := conn.Update(settings); err != nil {
|
if err := conn.Update(settings); err != nil {
|
||||||
return fmt.Errorf("failed to update connection: %w", err)
|
return fmt.Errorf("failed to update connection: %w", err)
|
||||||
}
|
}
|
||||||
@@ -1311,6 +1354,8 @@ func (b *NetworkManagerBackend) SetVPNCredentials(connUUID string, username stri
|
|||||||
delete(ipv6, "dns")
|
delete(ipv6, "dns")
|
||||||
}
|
}
|
||||||
|
|
||||||
|
mergeStoredSecrets(conn, settings)
|
||||||
|
|
||||||
if err := conn.Update(settings); err != nil {
|
if err := conn.Update(settings); err != nil {
|
||||||
return fmt.Errorf("failed to update connection: %w", err)
|
return fmt.Errorf("failed to update connection: %w", err)
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -956,6 +956,8 @@ func (b *NetworkManagerBackend) SetWiFiAutoconnect(ssid string, autoconnect bool
|
|||||||
delete(ipv6, "dns")
|
delete(ipv6, "dns")
|
||||||
}
|
}
|
||||||
|
|
||||||
|
mergeStoredSecrets(conn, settings)
|
||||||
|
|
||||||
err = conn.Update(settings)
|
err = conn.Update(settings)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return fmt.Errorf("failed to update connection: %w", err)
|
return fmt.Errorf("failed to update connection: %w", err)
|
||||||
|
|||||||
@@ -135,6 +135,8 @@ func (m *Manager) syncStateFromBackend() error {
|
|||||||
m.state.ConnectingSSID = backendState.ConnectingSSID
|
m.state.ConnectingSSID = backendState.ConnectingSSID
|
||||||
m.state.ConnectingDevice = backendState.ConnectingDevice
|
m.state.ConnectingDevice = backendState.ConnectingDevice
|
||||||
m.state.LastError = backendState.LastError
|
m.state.LastError = backendState.LastError
|
||||||
|
m.state.VPNError = backendState.VPNError
|
||||||
|
m.state.VPNErrorUuid = backendState.VPNErrorUuid
|
||||||
m.stateMutex.Unlock()
|
m.stateMutex.Unlock()
|
||||||
|
|
||||||
return nil
|
return nil
|
||||||
@@ -216,6 +218,9 @@ func stateChangedMeaningfully(old, new *NetworkState) bool {
|
|||||||
if old.LastError != new.LastError {
|
if old.LastError != new.LastError {
|
||||||
return true
|
return true
|
||||||
}
|
}
|
||||||
|
if old.VPNError != new.VPNError || old.VPNErrorUuid != new.VPNErrorUuid {
|
||||||
|
return true
|
||||||
|
}
|
||||||
if len(old.WiFiNetworks) != len(new.WiFiNetworks) {
|
if len(old.WiFiNetworks) != len(new.WiFiNetworks) {
|
||||||
return true
|
return true
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -121,6 +121,8 @@ type NetworkState struct {
|
|||||||
ConnectingSSID string `json:"connectingSSID"`
|
ConnectingSSID string `json:"connectingSSID"`
|
||||||
ConnectingDevice string `json:"connectingDevice,omitempty"`
|
ConnectingDevice string `json:"connectingDevice,omitempty"`
|
||||||
LastError string `json:"lastError"`
|
LastError string `json:"lastError"`
|
||||||
|
VPNError string `json:"vpnError"`
|
||||||
|
VPNErrorUuid string `json:"vpnErrorUuid"`
|
||||||
}
|
}
|
||||||
|
|
||||||
type ConnectionRequest struct {
|
type ConnectionRequest struct {
|
||||||
|
|||||||
@@ -689,23 +689,19 @@ Item {
|
|||||||
target: NetworkService
|
target: NetworkService
|
||||||
|
|
||||||
function onCredentialsNeeded(token, ssid, setting, fields, hints, reason, connType, connName, vpnService, fieldsInfo) {
|
function onCredentialsNeeded(token, ssid, setting, fields, hints, reason, connType, connName, vpnService, fieldsInfo) {
|
||||||
const now = Date.now();
|
const alreadyShown = wifiPasswordModalLoader.item && wifiPasswordModalLoader.item.shouldBeVisible;
|
||||||
const timeSinceLastPrompt = now - lastCredentialsTime;
|
if (alreadyShown && token === lastCredentialsToken)
|
||||||
|
return;
|
||||||
|
|
||||||
wifiPasswordModalLoader.active = true;
|
wifiPasswordModalLoader.active = true;
|
||||||
if (!wifiPasswordModalLoader.item)
|
if (!wifiPasswordModalLoader.item)
|
||||||
return;
|
return;
|
||||||
|
|
||||||
if (wifiPasswordModalLoader.item.shouldBeVisible && timeSinceLastPrompt < 1000) {
|
if (alreadyShown && lastCredentialsToken !== "" && lastCredentialsToken !== token)
|
||||||
NetworkService.cancelCredentials(lastCredentialsToken);
|
NetworkService.cancelCredentials(lastCredentialsToken);
|
||||||
lastCredentialsToken = token;
|
|
||||||
lastCredentialsTime = now;
|
|
||||||
wifiPasswordModalLoader.item.showFromPrompt(token, ssid, setting, fields, hints, reason, connType, connName, vpnService, fieldsInfo);
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
lastCredentialsToken = token;
|
lastCredentialsToken = token;
|
||||||
lastCredentialsTime = now;
|
lastCredentialsTime = Date.now();
|
||||||
wifiPasswordModalLoader.item.showFromPrompt(token, ssid, setting, fields, hints, reason, connType, connName, vpnService, fieldsInfo);
|
wifiPasswordModalLoader.item.showFromPrompt(token, ssid, setting, fields, hints, reason, connType, connName, vpnService, fieldsInfo);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -11,17 +11,22 @@ PluginComponent {
|
|||||||
service: DMSNetworkService
|
service: DMSNetworkService
|
||||||
}
|
}
|
||||||
|
|
||||||
|
readonly property bool vpnActivating: DMSNetworkService.vpnIsBusy || DMSNetworkService.activeState === "activating"
|
||||||
|
readonly property bool vpnActivated: DMSNetworkService.connected && DMSNetworkService.activeState === "activated"
|
||||||
|
|
||||||
ccWidgetIcon: "vpn_key"
|
ccWidgetIcon: "vpn_key"
|
||||||
ccWidgetPrimaryText: I18n.tr("VPN")
|
ccWidgetPrimaryText: I18n.tr("VPN")
|
||||||
ccWidgetSecondaryText: {
|
ccWidgetSecondaryText: {
|
||||||
if (!DMSNetworkService.connected)
|
if (vpnActivating)
|
||||||
|
return I18n.tr("Connecting…");
|
||||||
|
if (!vpnActivated)
|
||||||
return I18n.tr("Disconnected");
|
return I18n.tr("Disconnected");
|
||||||
const names = DMSNetworkService.activeNames || [];
|
const names = DMSNetworkService.activeNames || [];
|
||||||
if (names.length <= 1)
|
if (names.length <= 1)
|
||||||
return names[0] || I18n.tr("Connected");
|
return names[0] || I18n.tr("Connected");
|
||||||
return names[0] + " +" + (names.length - 1);
|
return names[0] + " +" + (names.length - 1);
|
||||||
}
|
}
|
||||||
ccWidgetIsActive: DMSNetworkService.connected
|
ccWidgetIsActive: vpnActivated
|
||||||
|
|
||||||
onCcWidgetToggled: DMSNetworkService.toggleVpn()
|
onCcWidgetToggled: DMSNetworkService.toggleVpn()
|
||||||
|
|
||||||
|
|||||||
@@ -84,6 +84,8 @@ Singleton {
|
|||||||
property string lastConnectedVpnUuid: ""
|
property string lastConnectedVpnUuid: ""
|
||||||
property string pendingVpnUuid: ""
|
property string pendingVpnUuid: ""
|
||||||
property var vpnBusyStartTime: 0
|
property var vpnBusyStartTime: 0
|
||||||
|
property string vpnError: ""
|
||||||
|
property string vpnErrorUuid: ""
|
||||||
|
|
||||||
property var profiles: {
|
property var profiles: {
|
||||||
const mergedProfiles = vpnProfiles ? vpnProfiles.slice() : [];
|
const mergedProfiles = vpnProfiles ? vpnProfiles.slice() : [];
|
||||||
@@ -137,6 +139,17 @@ Singleton {
|
|||||||
property alias isBusy: root.vpnIsBusy
|
property alias isBusy: root.vpnIsBusy
|
||||||
property alias connected: root.vpnConnected
|
property alias connected: root.vpnConnected
|
||||||
|
|
||||||
|
function vpnStateForUuid(uuid) {
|
||||||
|
if (!uuid)
|
||||||
|
return "";
|
||||||
|
const match = vpnActive.find(v => v.uuid === uuid);
|
||||||
|
return match ? (match.state || "") : "";
|
||||||
|
}
|
||||||
|
|
||||||
|
function isVpnConnectingUuid(uuid) {
|
||||||
|
return vpnStateForUuid(uuid) === "activating" || (vpnIsBusy && pendingVpnUuid === uuid);
|
||||||
|
}
|
||||||
|
|
||||||
property string networkInfoSSID: ""
|
property string networkInfoSSID: ""
|
||||||
property string networkInfoDetails: ""
|
property string networkInfoDetails: ""
|
||||||
property bool networkInfoLoading: false
|
property bool networkInfoLoading: false
|
||||||
@@ -372,6 +385,17 @@ Singleton {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const incomingVpnError = state.vpnError || "";
|
||||||
|
if (incomingVpnError && incomingVpnError !== vpnError) {
|
||||||
|
vpnIsBusy = false;
|
||||||
|
pendingVpnUuid = "";
|
||||||
|
vpnBusyStartTime = 0;
|
||||||
|
const failedName = (vpnProfiles.find(p => p.uuid === state.vpnErrorUuid)?.name) || I18n.tr("VPN");
|
||||||
|
ToastService.showError(I18n.tr("%1: %2").arg(failedName).arg(incomingVpnError));
|
||||||
|
}
|
||||||
|
vpnError = incomingVpnError;
|
||||||
|
vpnErrorUuid = state.vpnErrorUuid || "";
|
||||||
|
|
||||||
isConnecting = state.isConnecting || false;
|
isConnecting = state.isConnecting || false;
|
||||||
connectingSSID = state.connectingSSID || "";
|
connectingSSID = state.connectingSSID || "";
|
||||||
connectionError = state.lastError || "";
|
connectionError = state.lastError || "";
|
||||||
|
|||||||
@@ -25,6 +25,7 @@ Singleton {
|
|||||||
signal importComplete(string uuid, string name)
|
signal importComplete(string uuid, string name)
|
||||||
signal configLoaded(var config)
|
signal configLoaded(var config)
|
||||||
signal configUpdated
|
signal configUpdated
|
||||||
|
signal credentialsSet(string uuid)
|
||||||
signal vpnDeleted(string uuid)
|
signal vpnDeleted(string uuid)
|
||||||
|
|
||||||
Component.onCompleted: {
|
Component.onCompleted: {
|
||||||
@@ -149,6 +150,28 @@ Singleton {
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function setCredentials(uuid, username, password, save = true) {
|
||||||
|
if (!available)
|
||||||
|
return;
|
||||||
|
const params = {
|
||||||
|
uuid: uuid,
|
||||||
|
save: save
|
||||||
|
};
|
||||||
|
if (username)
|
||||||
|
params.username = username;
|
||||||
|
if (password)
|
||||||
|
params.password = password;
|
||||||
|
|
||||||
|
DMSService.sendRequest("network.vpn.setCredentials", params, response => {
|
||||||
|
if (response.error) {
|
||||||
|
ToastService.showError(I18n.tr("Failed to save VPN credentials"), response.error);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
ToastService.showInfo(I18n.tr("VPN credentials saved"));
|
||||||
|
credentialsSet(uuid);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
function deleteVpn(uuidOrName) {
|
function deleteVpn(uuidOrName) {
|
||||||
if (!available)
|
if (!available)
|
||||||
return;
|
return;
|
||||||
|
|||||||
@@ -18,7 +18,9 @@ Rectangle {
|
|||||||
signal toggleExpand
|
signal toggleExpand
|
||||||
signal deleteRequested
|
signal deleteRequested
|
||||||
|
|
||||||
readonly property bool isActive: DMSNetworkService.activeUuids?.includes(profile?.uuid) ?? false
|
readonly property bool isActive: DMSNetworkService.vpnStateForUuid(profile?.uuid) === "activated"
|
||||||
|
readonly property bool isConnecting: DMSNetworkService.isVpnConnectingUuid(profile?.uuid)
|
||||||
|
readonly property bool hasError: !isConnecting && DMSNetworkService.vpnError !== "" && DMSNetworkService.vpnErrorUuid === (profile?.uuid ?? "")
|
||||||
readonly property bool isHovered: rowArea.containsMouse || expandBtn.containsMouse || deleteBtn.containsMouse
|
readonly property bool isHovered: rowArea.containsMouse || expandBtn.containsMouse || deleteBtn.containsMouse
|
||||||
readonly property var configData: (!isTransient && isExpanded) ? VPNService.editConfig : null
|
readonly property var configData: (!isTransient && isExpanded) ? VPNService.editConfig : null
|
||||||
readonly property var configFields: buildConfigFields()
|
readonly property var configFields: buildConfigFields()
|
||||||
@@ -28,7 +30,7 @@ Rectangle {
|
|||||||
color: isHovered ? Theme.primaryHoverLight : (isActive ? Theme.primaryPressed : Theme.surfaceLight)
|
color: isHovered ? Theme.primaryHoverLight : (isActive ? Theme.primaryPressed : Theme.surfaceLight)
|
||||||
border.width: isActive ? 2 : 1
|
border.width: isActive ? 2 : 1
|
||||||
border.color: isActive ? Theme.primary : Theme.outlineLight
|
border.color: isActive ? Theme.primary : Theme.outlineLight
|
||||||
opacity: DMSNetworkService.isBusy ? 0.5 : 1.0
|
opacity: (DMSNetworkService.isBusy && !isConnecting) ? 0.5 : 1.0
|
||||||
clip: true
|
clip: true
|
||||||
|
|
||||||
function buildConfigFields() {
|
function buildConfigFields() {
|
||||||
@@ -107,10 +109,20 @@ Rectangle {
|
|||||||
height: 46 - Theme.spacingS * 2
|
height: 46 - Theme.spacingS * 2
|
||||||
spacing: Theme.spacingS
|
spacing: Theme.spacingS
|
||||||
|
|
||||||
|
DankSpinner {
|
||||||
|
size: 18
|
||||||
|
strokeWidth: 2
|
||||||
|
color: Theme.warning
|
||||||
|
running: root.isConnecting
|
||||||
|
visible: root.isConnecting
|
||||||
|
anchors.verticalCenter: parent.verticalCenter
|
||||||
|
}
|
||||||
|
|
||||||
DankIcon {
|
DankIcon {
|
||||||
name: isActive ? "vpn_lock" : "vpn_key_off"
|
visible: !root.isConnecting
|
||||||
|
name: isActive ? "vpn_lock" : (root.hasError ? "error" : "vpn_key_off")
|
||||||
size: 20
|
size: 20
|
||||||
color: isActive ? Theme.primary : Theme.surfaceText
|
color: root.hasError ? Theme.error : (isActive ? Theme.primary : Theme.surfaceText)
|
||||||
anchors.verticalCenter: parent.verticalCenter
|
anchors.verticalCenter: parent.verticalCenter
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -130,9 +142,9 @@ Rectangle {
|
|||||||
}
|
}
|
||||||
|
|
||||||
StyledText {
|
StyledText {
|
||||||
text: VPNService.getVpnTypeFromProfile(profile)
|
text: root.isConnecting ? I18n.tr("Connecting...") : (root.hasError ? DMSNetworkService.vpnError : VPNService.getVpnTypeFromProfile(profile))
|
||||||
font.pixelSize: Theme.fontSizeSmall
|
font.pixelSize: Theme.fontSizeSmall
|
||||||
color: Theme.surfaceTextMedium
|
color: root.isConnecting ? Theme.warning : (root.hasError ? Theme.error : Theme.surfaceTextMedium)
|
||||||
wrapMode: Text.NoWrap
|
wrapMode: Text.NoWrap
|
||||||
width: parent.width
|
width: parent.width
|
||||||
elide: Text.ElideRight
|
elide: Text.ElideRight
|
||||||
@@ -271,6 +283,45 @@ Rectangle {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
Column {
|
||||||
|
width: parent.width
|
||||||
|
spacing: Theme.spacingXS
|
||||||
|
visible: !isTransient && !VPNService.configLoading && profile?.type !== "wireguard"
|
||||||
|
|
||||||
|
StyledText {
|
||||||
|
text: root.hasError ? DMSNetworkService.vpnError : I18n.tr("Credentials")
|
||||||
|
font.pixelSize: Theme.fontSizeSmall
|
||||||
|
color: root.hasError ? Theme.error : Theme.surfaceVariantText
|
||||||
|
}
|
||||||
|
|
||||||
|
DankTextField {
|
||||||
|
id: usernameField
|
||||||
|
width: parent.width
|
||||||
|
placeholderText: I18n.tr("Username")
|
||||||
|
text: (configData && (configData.username || (configData.data && configData.data.username))) || ""
|
||||||
|
}
|
||||||
|
|
||||||
|
DankTextField {
|
||||||
|
id: passwordField
|
||||||
|
width: parent.width
|
||||||
|
placeholderText: I18n.tr("Password")
|
||||||
|
echoMode: TextInput.Password
|
||||||
|
showPasswordToggle: true
|
||||||
|
normalBorderColor: root.hasError ? Theme.error : Theme.outlineMedium
|
||||||
|
}
|
||||||
|
|
||||||
|
DankButton {
|
||||||
|
text: I18n.tr("Save credentials")
|
||||||
|
opacity: passwordField.text.length > 0 ? 1 : 0.5
|
||||||
|
onClicked: {
|
||||||
|
if (passwordField.text.length === 0)
|
||||||
|
return;
|
||||||
|
VPNService.setCredentials(profile.uuid, usernameField.text, passwordField.text, true);
|
||||||
|
passwordField.text = "";
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
Item {
|
Item {
|
||||||
width: 1
|
width: 1
|
||||||
height: Theme.spacingXS
|
height: Theme.spacingXS
|
||||||
|
|||||||
Reference in New Issue
Block a user