From df0717dc167201ab543c2b68252ba1104c6f4587 Mon Sep 17 00:00:00 2001 From: "Theodor S. Midtlien" Date: Tue, 2 Jun 2026 09:58:28 +0200 Subject: [PATCH] Migrate android profile manager --- client/android/profile_manager.go | 80 +++++++++---------- client/internal/profilemanager/id.go | 4 +- .../internal/profilemanager/profilemanager.go | 4 +- client/internal/profilemanager/service.go | 13 ++- .../internal/profilemanager/service_test.go | 2 +- client/internal/profilemanager/state.go | 4 +- 6 files changed, 50 insertions(+), 57 deletions(-) diff --git a/client/android/profile_manager.go b/client/android/profile_manager.go index 60e4d5c32..e7c58c723 100644 --- a/client/android/profile_manager.go +++ b/client/android/profile_manager.go @@ -6,7 +6,6 @@ import ( "fmt" "os" "path/filepath" - "strings" log "github.com/sirupsen/logrus" @@ -24,6 +23,7 @@ const ( // Profile represents a profile for gomobile type Profile struct { + ID string Name string IsActive bool } @@ -99,6 +99,7 @@ func (pm *ProfileManager) ListProfiles() (*ProfileArray, error) { var profiles []*Profile for _, p := range internalProfiles { profiles = append(profiles, &Profile{ + ID: p.ID, Name: p.Name, IsActive: p.IsActive, }) @@ -108,55 +109,65 @@ func (pm *ProfileManager) ListProfiles() (*ProfileArray, error) { } // GetActiveProfile returns the currently active profile name -func (pm *ProfileManager) GetActiveProfile() (string, error) { +func (pm *ProfileManager) GetActiveProfile() (*Profile, error) { // Use ServiceManager to stay consistent with ListProfiles // ServiceManager uses active_profile.json activeState, err := pm.serviceMgr.GetActiveProfileState() if err != nil { - return "", fmt.Errorf("failed to get active profile: %w", err) + return nil, fmt.Errorf("failed to get active profile: %w", err) } - return activeState.Name, nil + + // ActiveProfileState only stores the ID (and username), not the display + // name. Resolve the ID to the full profile so callers get the real Name. + prof, err := pm.serviceMgr.ResolveProfile(activeState.ID, androidUsername) + if err != nil { + return nil, fmt.Errorf("failed to resolve active profile %q: %w", activeState.ID, err) + } + return &Profile{ID: prof.ID, Name: prof.Name, IsActive: true}, nil } // SwitchProfile switches to a different profile -func (pm *ProfileManager) SwitchProfile(profileName string) error { +func (pm *ProfileManager) SwitchProfile(id string) error { // Use ServiceManager to stay consistent with ListProfiles // ServiceManager uses active_profile.json err := pm.serviceMgr.SetActiveProfileState(&profilemanager.ActiveProfileState{ - Name: profileName, + ID: id, Username: androidUsername, }) if err != nil { return fmt.Errorf("failed to switch profile: %w", err) } - log.Infof("switched to profile: %s", profileName) + log.Infof("switched to profile: %s", id) return nil } // AddProfile creates a new profile func (pm *ProfileManager) AddProfile(profileName string) error { // Use ServiceManager (creates profile in profiles/ directory) - if err := pm.serviceMgr.AddProfile(profileName, androidUsername); err != nil { + profile, err := pm.serviceMgr.AddProfile(profileName, androidUsername) + if err != nil { return fmt.Errorf("failed to add profile: %w", err) } - log.Infof("created new profile: %s", profileName) + log.Infof("created new profile: %s", profile.ID) return nil } // LogoutProfile logs out from a profile (clears authentication) -func (pm *ProfileManager) LogoutProfile(profileName string) error { - profileName = sanitizeProfileName(profileName) - - configPath, err := pm.getProfileConfigPath(profileName) +func (pm *ProfileManager) LogoutProfile(id string) error { + configPath, err := pm.getProfileConfigPath(id) if err != nil { return err } + if !profilemanager.IsValidProfileFilenameStem(id) { + return fmt.Errorf("id '%s' is not valid", id) + } + // Check if profile exists if _, err := os.Stat(configPath); os.IsNotExist(err) { - return fmt.Errorf("profile '%s' does not exist", profileName) + return fmt.Errorf("profile '%s' does not exist", id) } // Read current config using internal profilemanager @@ -174,35 +185,32 @@ func (pm *ProfileManager) LogoutProfile(profileName string) error { return fmt.Errorf("failed to save config: %w", err) } - log.Infof("logged out from profile: %s", profileName) + log.Infof("logged out from profile: %s", id) return nil } // RemoveProfile deletes a profile -func (pm *ProfileManager) RemoveProfile(profileName string) error { +func (pm *ProfileManager) RemoveProfile(id string) error { // Use ServiceManager (removes profile from profiles/ directory) - if err := pm.serviceMgr.RemoveProfile(profileName, androidUsername); err != nil { + if err := pm.serviceMgr.RemoveProfile(id, androidUsername); err != nil { return fmt.Errorf("failed to remove profile: %w", err) } - log.Infof("removed profile: %s", profileName) + log.Infof("removed profile: %s", id) return nil } // getProfileConfigPath returns the config file path for a profile // This is needed for Android-specific path handling (netbird.cfg for default profile) -func (pm *ProfileManager) getProfileConfigPath(profileName string) (string, error) { - if profileName == "" || profileName == profilemanager.DefaultProfileName { +func (pm *ProfileManager) getProfileConfigPath(id string) (string, error) { + if id == "" || id == profilemanager.DefaultProfileName { // Android uses netbird.cfg for default profile instead of default.json // Default profile is stored in root configDir, not in profiles/ return filepath.Join(pm.configDir, defaultConfigFilename), nil } - // Non-default profiles are stored in profiles subdirectory - // This matches the Java Preferences.java expectation - profileName = sanitizeProfileName(profileName) profilesDir := filepath.Join(pm.configDir, profilesSubdir) - return filepath.Join(profilesDir, profileName+".json"), nil + return filepath.Join(profilesDir, id+".json"), nil } // GetConfigPath returns the config file path for a given profile @@ -213,14 +221,13 @@ func (pm *ProfileManager) GetConfigPath(profileName string) (string, error) { // GetStateFilePath returns the state file path for a given profile // Java should call this instead of constructing paths with Preferences.stateFile() -func (pm *ProfileManager) GetStateFilePath(profileName string) (string, error) { - if profileName == "" || profileName == profilemanager.DefaultProfileName { +func (pm *ProfileManager) GetStateFilePath(id string) (string, error) { + if id == "" || id == profilemanager.DefaultProfileName { return filepath.Join(pm.configDir, "state.json"), nil } - profileName = sanitizeProfileName(profileName) profilesDir := filepath.Join(pm.configDir, profilesSubdir) - return filepath.Join(profilesDir, profileName+".state.json"), nil + return filepath.Join(profilesDir, id+".state.json"), nil } // GetActiveConfigPath returns the config file path for the currently active profile @@ -230,7 +237,7 @@ func (pm *ProfileManager) GetActiveConfigPath() (string, error) { if err != nil { return "", fmt.Errorf("failed to get active profile: %w", err) } - return pm.GetConfigPath(activeProfile) + return pm.GetConfigPath(activeProfile.ID) } // GetActiveStateFilePath returns the state file path for the currently active profile @@ -240,18 +247,5 @@ func (pm *ProfileManager) GetActiveStateFilePath() (string, error) { if err != nil { return "", fmt.Errorf("failed to get active profile: %w", err) } - return pm.GetStateFilePath(activeProfile) -} - -// sanitizeProfileName removes invalid characters from profile name -func sanitizeProfileName(name string) string { - // Keep only alphanumeric, underscore, and hyphen - var result strings.Builder - for _, r := range name { - if (r >= 'a' && r <= 'z') || (r >= 'A' && r <= 'Z') || - (r >= '0' && r <= '9') || r == '_' || r == '-' { - result.WriteRune(r) - } - } - return result.String() + return pm.GetStateFilePath(activeProfile.ID) } diff --git a/client/internal/profilemanager/id.go b/client/internal/profilemanager/id.go index b1a15c1f3..808ca814b 100644 --- a/client/internal/profilemanager/id.go +++ b/client/internal/profilemanager/id.go @@ -40,9 +40,9 @@ func generateProfileID() (string, error) { return hex.EncodeToString(buf), nil } -// isValidProfileFilenameStem reports whether s is safe to use as the stem +// IsValidProfileFilenameStem reports whether s is safe to use as the stem // of a profile JSON filename. -func isValidProfileFilenameStem(s string) bool { +func IsValidProfileFilenameStem(s string) bool { if s == "" || len(s) > maxProfileIDLen { return false } diff --git a/client/internal/profilemanager/profilemanager.go b/client/internal/profilemanager/profilemanager.go index 9b3c82478..3c6c76c5a 100644 --- a/client/internal/profilemanager/profilemanager.go +++ b/client/internal/profilemanager/profilemanager.go @@ -50,7 +50,7 @@ func (p *Profile) FilePath() (string, error) { return DefaultConfigPath, nil } - if !isValidProfileFilenameStem(id) { + if !IsValidProfileFilenameStem(id) { return "", fmt.Errorf("invalid profile ID: %q", id) } @@ -99,7 +99,7 @@ func (pm *ProfileManager) GetActiveProfile() (*Profile, error) { // supplied handle to a concrete ID, so the value written here is always a // valid profile filename stem. func (pm *ProfileManager) SwitchProfile(id string) error { - if id != defaultProfileName && !isValidProfileFilenameStem(id) { + if id != defaultProfileName && !IsValidProfileFilenameStem(id) { return fmt.Errorf("invalid profile ID: %q", id) } diff --git a/client/internal/profilemanager/service.go b/client/internal/profilemanager/service.go index b3c1dda9b..f0aeecc78 100644 --- a/client/internal/profilemanager/service.go +++ b/client/internal/profilemanager/service.go @@ -105,7 +105,7 @@ func (a *ActiveProfileState) FilePath() (string, error) { return DefaultConfigPath, nil } - if !isValidProfileFilenameStem(a.ID) { + if !IsValidProfileFilenameStem(a.ID) { return "", fmt.Errorf("invalid profile ID: %q", a.ID) } @@ -266,7 +266,7 @@ func (s *ServiceManager) SetActiveProfileState(a *ActiveProfileState) error { return fmt.Errorf("username must be set for non-default profiles, got: %s", a.ID) } - if a.ID != defaultProfileName && !isValidProfileFilenameStem(a.ID) { + if a.ID != defaultProfileName && !IsValidProfileFilenameStem(a.ID) { return fmt.Errorf("invalid profile ID: %q", a.ID) } @@ -341,7 +341,7 @@ func (s *ServiceManager) RemoveProfile(id, username string) error { if id == defaultProfileName { return fmt.Errorf("cannot remove profile with reserved name: %s", defaultProfileName) } - if !isValidProfileFilenameStem(id) { + if !IsValidProfileFilenameStem(id) { return fmt.Errorf("invalid profile ID: %q", id) } @@ -408,7 +408,7 @@ func (s *ServiceManager) GetStatePath() string { return defaultStatePath } - if !isValidProfileFilenameStem(activeProf.ID) { + if !IsValidProfileFilenameStem(activeProf.ID) { log.Warnf("invalid active profile ID %q, using default state path", activeProf.ID) return defaultStatePath } @@ -478,7 +478,7 @@ func (s *ServiceManager) loadAllProfiles(username string) ([]Profile, error) { // default lives at the top-level config dir, not under / continue } - if !isValidProfileFilenameStem(stem) { + if !IsValidProfileFilenameStem(stem) { continue } path := filepath.Join(configDir, base) @@ -498,8 +498,7 @@ func (s *ServiceManager) loadAllProfiles(username string) ([]Profile, error) { if fileProfiles[i].Name != fileProfiles[j].Name { return fileProfiles[i].Name < fileProfiles[j].Name } - // Stable tie-break on ID so duplicate names always render in - // the same order across calls. + // Sort tie-break on ID so duplicate names always render in the same order. return fileProfiles[i].ID < fileProfiles[j].ID }) profiles = append(profiles, fileProfiles...) diff --git a/client/internal/profilemanager/service_test.go b/client/internal/profilemanager/service_test.go index 8c3f3e00e..60fea7c3e 100644 --- a/client/internal/profilemanager/service_test.go +++ b/client/internal/profilemanager/service_test.go @@ -218,7 +218,7 @@ func TestIsValidProfileFilenameStem(t *testing.T) { {strings.Repeat("a", maxProfileIDLen+1), false}, } for _, tc := range cases { - got := isValidProfileFilenameStem(tc.in) + got := IsValidProfileFilenameStem(tc.in) assert.Equal(t, tc.want, got, "case %q", tc.in) } } diff --git a/client/internal/profilemanager/state.go b/client/internal/profilemanager/state.go index 5caa85bd4..4729f992e 100644 --- a/client/internal/profilemanager/state.go +++ b/client/internal/profilemanager/state.go @@ -22,7 +22,7 @@ func (pm *ProfileManager) GetProfileState(id string) (*ProfileState, error) { return nil, fmt.Errorf("get config directory: %w", err) } - if id != defaultProfileName && !isValidProfileFilenameStem(id) { + if id != defaultProfileName && !IsValidProfileFilenameStem(id) { return nil, fmt.Errorf("invalid profile ID: %q", id) } @@ -59,7 +59,7 @@ func (pm *ProfileManager) SetActiveProfileState(state *ProfileState) error { } id := activeProf.ID - if id != defaultProfileName && !isValidProfileFilenameStem(id) { + if id != defaultProfileName && !IsValidProfileFilenameStem(id) { return fmt.Errorf("invalid active profile ID: %q", id) }