mirror of
https://github.com/netbirdio/netbird.git
synced 2025-08-06 22:16:54 +02:00
360 lines
9.7 KiB
Go
360 lines
9.7 KiB
Go
package profilemanager
|
|
|
|
import (
|
|
"context"
|
|
"errors"
|
|
"fmt"
|
|
"io"
|
|
"os"
|
|
"path/filepath"
|
|
"runtime"
|
|
"sort"
|
|
"strings"
|
|
|
|
log "github.com/sirupsen/logrus"
|
|
|
|
"github.com/netbirdio/netbird/util"
|
|
)
|
|
|
|
var (
|
|
oldDefaultConfigPathDir = ""
|
|
oldDefaultConfigPath = ""
|
|
|
|
DefaultConfigPathDir = ""
|
|
DefaultConfigPath = ""
|
|
ActiveProfileStatePath = ""
|
|
)
|
|
|
|
var (
|
|
ErrorOldDefaultConfigNotFound = errors.New("old default config not found")
|
|
)
|
|
|
|
func init() {
|
|
|
|
DefaultConfigPathDir = "/var/lib/netbird/"
|
|
oldDefaultConfigPathDir = "/etc/netbird/"
|
|
|
|
switch runtime.GOOS {
|
|
case "windows":
|
|
oldDefaultConfigPathDir = filepath.Join(os.Getenv("PROGRAMDATA"), "Netbird")
|
|
DefaultConfigPathDir = oldDefaultConfigPathDir
|
|
|
|
case "freebsd":
|
|
oldDefaultConfigPathDir = "/var/db/netbird/"
|
|
DefaultConfigPathDir = oldDefaultConfigPathDir
|
|
}
|
|
|
|
oldDefaultConfigPath = filepath.Join(oldDefaultConfigPathDir, "config.json")
|
|
DefaultConfigPath = filepath.Join(DefaultConfigPathDir, "default.json")
|
|
ActiveProfileStatePath = filepath.Join(DefaultConfigPathDir, "active_profile.json")
|
|
}
|
|
|
|
type ActiveProfileState struct {
|
|
Name string `json:"name"`
|
|
Username string `json:"username"`
|
|
}
|
|
|
|
func (a *ActiveProfileState) FilePath() (string, error) {
|
|
if a.Name == "" {
|
|
return "", fmt.Errorf("active profile name is empty")
|
|
}
|
|
|
|
if a.Name == defaultProfileName {
|
|
return DefaultConfigPath, nil
|
|
}
|
|
|
|
configDir, err := getConfigDirForUser(a.Username)
|
|
if err != nil {
|
|
return "", fmt.Errorf("failed to get config directory for user %s: %w", a.Username, err)
|
|
}
|
|
|
|
return filepath.Join(configDir, a.Name+".json"), nil
|
|
}
|
|
|
|
type ServiceManager struct{}
|
|
|
|
func (s *ServiceManager) CopyDefaultProfileIfNotExists() (bool, error) {
|
|
|
|
if err := os.MkdirAll(DefaultConfigPathDir, 0600); err != nil {
|
|
return false, fmt.Errorf("failed to create default config path directory: %w", err)
|
|
}
|
|
|
|
// check if default profile exists
|
|
if _, err := os.Stat(DefaultConfigPath); !os.IsNotExist(err) {
|
|
// default profile already exists
|
|
log.Debugf("default profile already exists at %s, skipping copy", DefaultConfigPath)
|
|
return false, nil
|
|
}
|
|
|
|
// check old default profile
|
|
if _, err := os.Stat(oldDefaultConfigPath); os.IsNotExist(err) {
|
|
// old default profile does not exist, nothing to copy
|
|
return false, ErrorOldDefaultConfigNotFound
|
|
}
|
|
|
|
// copy old default profile to new location
|
|
if err := copyFile(oldDefaultConfigPath, DefaultConfigPath, 0600); err != nil {
|
|
return false, fmt.Errorf("copy default profile from %s to %s: %w", oldDefaultConfigPath, DefaultConfigPath, err)
|
|
}
|
|
|
|
// set permissions for the new default profile
|
|
if err := os.Chmod(DefaultConfigPath, 0600); err != nil {
|
|
log.Warnf("failed to set permissions for default profile: %v", err)
|
|
}
|
|
|
|
if err := s.SetActiveProfileState(&ActiveProfileState{
|
|
Name: "default",
|
|
Username: "",
|
|
}); err != nil {
|
|
log.Errorf("failed to set active profile state: %v", err)
|
|
return false, fmt.Errorf("failed to set active profile state: %w", err)
|
|
}
|
|
|
|
return true, nil
|
|
}
|
|
|
|
// copyFile copies the contents of src to dst and sets dst's file mode to perm.
|
|
func copyFile(src, dst string, perm os.FileMode) error {
|
|
in, err := os.Open(src)
|
|
if err != nil {
|
|
return fmt.Errorf("open source file %s: %w", src, err)
|
|
}
|
|
defer in.Close()
|
|
|
|
out, err := os.OpenFile(dst, os.O_CREATE|os.O_WRONLY|os.O_TRUNC, perm)
|
|
if err != nil {
|
|
return fmt.Errorf("open target file %s: %w", dst, err)
|
|
}
|
|
defer func() {
|
|
if cerr := out.Close(); cerr != nil && err == nil {
|
|
err = cerr
|
|
}
|
|
}()
|
|
|
|
if _, err := io.Copy(out, in); err != nil {
|
|
return fmt.Errorf("copy data to %s: %w", dst, err)
|
|
}
|
|
|
|
return nil
|
|
}
|
|
|
|
func (s *ServiceManager) CreateDefaultProfile() error {
|
|
_, err := UpdateOrCreateConfig(ConfigInput{
|
|
ConfigPath: DefaultConfigPath,
|
|
})
|
|
|
|
if err != nil {
|
|
return fmt.Errorf("failed to create default profile: %w", err)
|
|
}
|
|
|
|
log.Infof("default profile created at %s", DefaultConfigPath)
|
|
return nil
|
|
}
|
|
|
|
func (s *ServiceManager) GetActiveProfileState() (*ActiveProfileState, error) {
|
|
if err := s.setDefaultActiveState(); err != nil {
|
|
return nil, fmt.Errorf("failed to set default active profile state: %w", err)
|
|
}
|
|
var activeProfile ActiveProfileState
|
|
if _, err := util.ReadJson(ActiveProfileStatePath, &activeProfile); err != nil {
|
|
if errors.Is(err, os.ErrNotExist) {
|
|
if err := s.SetActiveProfileStateToDefault(); err != nil {
|
|
return nil, fmt.Errorf("failed to set active profile to default: %w", err)
|
|
}
|
|
return &ActiveProfileState{
|
|
Name: "default",
|
|
Username: "",
|
|
}, nil
|
|
} else {
|
|
return nil, fmt.Errorf("failed to read active profile state: %w", err)
|
|
}
|
|
}
|
|
|
|
if activeProfile.Name == "" {
|
|
if err := s.SetActiveProfileStateToDefault(); err != nil {
|
|
return nil, fmt.Errorf("failed to set active profile to default: %w", err)
|
|
}
|
|
return &ActiveProfileState{
|
|
Name: "default",
|
|
Username: "",
|
|
}, nil
|
|
}
|
|
|
|
return &activeProfile, nil
|
|
|
|
}
|
|
|
|
func (s *ServiceManager) setDefaultActiveState() error {
|
|
_, err := os.Stat(ActiveProfileStatePath)
|
|
if err != nil {
|
|
if os.IsNotExist(err) {
|
|
if err := s.SetActiveProfileStateToDefault(); err != nil {
|
|
return fmt.Errorf("failed to set active profile to default: %w", err)
|
|
}
|
|
} else {
|
|
return fmt.Errorf("failed to stat active profile state path %s: %w", ActiveProfileStatePath, err)
|
|
}
|
|
}
|
|
|
|
return nil
|
|
}
|
|
|
|
func (s *ServiceManager) SetActiveProfileState(a *ActiveProfileState) error {
|
|
if a == nil || a.Name == "" {
|
|
return errors.New("invalid active profile state")
|
|
}
|
|
|
|
if a.Name != defaultProfileName && a.Username == "" {
|
|
return fmt.Errorf("username must be set for non-default profiles, got: %s", a.Name)
|
|
}
|
|
|
|
if err := util.WriteJsonWithRestrictedPermission(context.Background(), ActiveProfileStatePath, a); err != nil {
|
|
return fmt.Errorf("failed to write active profile state: %w", err)
|
|
}
|
|
|
|
log.Infof("active profile set to %s for %s", a.Name, a.Username)
|
|
return nil
|
|
}
|
|
|
|
func (s *ServiceManager) SetActiveProfileStateToDefault() error {
|
|
return s.SetActiveProfileState(&ActiveProfileState{
|
|
Name: "default",
|
|
Username: "",
|
|
})
|
|
}
|
|
|
|
func (s *ServiceManager) DefaultProfilePath() string {
|
|
return DefaultConfigPath
|
|
}
|
|
|
|
func (s *ServiceManager) AddProfile(profileName, username string) error {
|
|
configDir, err := getConfigDirForUser(username)
|
|
if err != nil {
|
|
return fmt.Errorf("failed to get config directory: %w", err)
|
|
}
|
|
|
|
profileName = sanitizeProfileName(profileName)
|
|
|
|
if profileName == defaultProfileName {
|
|
return fmt.Errorf("cannot create profile with reserved name: %s", defaultProfileName)
|
|
}
|
|
|
|
profPath := filepath.Join(configDir, profileName+".json")
|
|
if fileExists(profPath) {
|
|
return ErrProfileAlreadyExists
|
|
}
|
|
|
|
cfg, err := createNewConfig(ConfigInput{ConfigPath: profPath})
|
|
if err != nil {
|
|
return fmt.Errorf("failed to create new config: %w", err)
|
|
}
|
|
|
|
err = util.WriteJson(context.Background(), profPath, cfg)
|
|
if err != nil {
|
|
return fmt.Errorf("failed to write profile config: %w", err)
|
|
}
|
|
|
|
return nil
|
|
}
|
|
|
|
func (s *ServiceManager) RemoveProfile(profileName, username string) error {
|
|
configDir, err := getConfigDirForUser(username)
|
|
if err != nil {
|
|
return fmt.Errorf("failed to get config directory: %w", err)
|
|
}
|
|
|
|
profileName = sanitizeProfileName(profileName)
|
|
|
|
if profileName == defaultProfileName {
|
|
return fmt.Errorf("cannot remove profile with reserved name: %s", defaultProfileName)
|
|
}
|
|
profPath := filepath.Join(configDir, profileName+".json")
|
|
if !fileExists(profPath) {
|
|
return ErrProfileNotFound
|
|
}
|
|
|
|
activeProf, err := s.GetActiveProfileState()
|
|
if err != nil && !errors.Is(err, ErrNoActiveProfile) {
|
|
return fmt.Errorf("failed to get active profile: %w", err)
|
|
}
|
|
|
|
if activeProf != nil && activeProf.Name == profileName {
|
|
return fmt.Errorf("cannot remove active profile: %s", profileName)
|
|
}
|
|
|
|
err = util.RemoveJson(profPath)
|
|
if err != nil {
|
|
return fmt.Errorf("failed to remove profile config: %w", err)
|
|
}
|
|
return nil
|
|
}
|
|
|
|
func (s *ServiceManager) ListProfiles(username string) ([]Profile, error) {
|
|
configDir, err := getConfigDirForUser(username)
|
|
if err != nil {
|
|
return nil, fmt.Errorf("failed to get config directory: %w", err)
|
|
}
|
|
|
|
files, err := util.ListFiles(configDir, "*.json")
|
|
if err != nil {
|
|
return nil, fmt.Errorf("failed to list profile files: %w", err)
|
|
}
|
|
|
|
var filtered []string
|
|
for _, file := range files {
|
|
if strings.HasSuffix(file, "state.json") {
|
|
continue // skip state files
|
|
}
|
|
filtered = append(filtered, file)
|
|
}
|
|
sort.Strings(filtered)
|
|
|
|
var activeProfName string
|
|
activeProf, err := s.GetActiveProfileState()
|
|
if err == nil {
|
|
activeProfName = activeProf.Name
|
|
}
|
|
|
|
var profiles []Profile
|
|
// add default profile always
|
|
profiles = append(profiles, Profile{Name: defaultProfileName, IsActive: activeProfName == "" || activeProfName == defaultProfileName})
|
|
for _, file := range filtered {
|
|
profileName := strings.TrimSuffix(filepath.Base(file), ".json")
|
|
var isActive bool
|
|
if activeProfName != "" && activeProfName == profileName {
|
|
isActive = true
|
|
}
|
|
profiles = append(profiles, Profile{Name: profileName, IsActive: isActive})
|
|
}
|
|
|
|
return profiles, nil
|
|
}
|
|
|
|
// GetStatePath returns the path to the state file based on the operating system
|
|
// It returns an empty string if the path cannot be determined.
|
|
func (s *ServiceManager) GetStatePath() string {
|
|
if path := os.Getenv("NB_DNS_STATE_FILE"); path != "" {
|
|
return path
|
|
}
|
|
|
|
defaultStatePath := filepath.Join(DefaultConfigPathDir, "state.json")
|
|
|
|
activeProf, err := s.GetActiveProfileState()
|
|
if err != nil {
|
|
log.Warnf("failed to get active profile state: %v", err)
|
|
return defaultStatePath
|
|
}
|
|
|
|
if activeProf.Name == defaultProfileName {
|
|
return defaultStatePath
|
|
}
|
|
|
|
configDir, err := getConfigDirForUser(activeProf.Username)
|
|
if err != nil {
|
|
log.Warnf("failed to get config directory for user %s: %v", activeProf.Username, err)
|
|
return defaultStatePath
|
|
}
|
|
|
|
return filepath.Join(configDir, activeProf.Name+".state.json")
|
|
}
|