package config import ( "fmt" "strings" ) // ProfileInfo is a safe summary for UI lists (no secrets beyond proxy URI the user already owns). type ProfileInfo struct { Name string `json:"name"` Protocol string `json:"protocol"` Proxy string `json:"proxy"` Host string `json:"host"` Active bool `json:"active"` } // ListProfiles returns UI-friendly profile summaries. func (c *Config) ListProfiles() []ProfileInfo { out := make([]ProfileInfo, 0, len(c.Profiles)) for _, p := range c.Profiles { out = append(out, ProfileInfo{ Name: p.Name, Protocol: string(p.Protocol), Proxy: p.Proxy, Host: proxyHost(p.Proxy), Active: p.Name == c.Active, }) } return out } // ListProfilesForPoll is like ListProfiles but omits Proxy URIs (cheaper idle polls). func (c *Config) ListProfilesForPoll() []ProfileInfo { out := make([]ProfileInfo, 0, len(c.Profiles)) for _, p := range c.Profiles { out = append(out, ProfileInfo{ Name: p.Name, Protocol: string(p.Protocol), Host: proxyHost(p.Proxy), Active: p.Name == c.Active, }) } return out } // RedactProfileList clears Proxy URIs from a profile list (host/protocol remain). // Use for periodic UI polls so credentials are not echoed for every profile. func RedactProfileList(in []ProfileInfo) []ProfileInfo { out := make([]ProfileInfo, len(in)) for i, p := range in { out[i] = p out[i].Proxy = "" } return out } func RedactProxyURI(proxy string) string { proxy = strings.TrimSpace(proxy) if proxy == "" { return "" } lower := strings.ToLower(proxy) if strings.Contains(lower, "[interface]") || strings.Contains(lower, "privatekey") { return "[конфиг скрыт — откройте редактор профиля]" } if i := strings.Index(proxy, "://"); i >= 0 { scheme := proxy[:i+3] rest := proxy[i+3:] if at := strings.LastIndex(rest, "@"); at >= 0 { return scheme + "***@" + rest[at+1:] } return scheme + rest } if at := strings.LastIndex(proxy, "@"); at >= 0 { return "***@" + proxy[at+1:] } return proxyHost(proxy) } func proxyHost(proxy string) string { proxy = strings.TrimSpace(proxy) if proxy == "" { return "" } lower := strings.ToLower(proxy) if strings.Contains(lower, "[interface]") { for _, line := range strings.Split(proxy, "\n") { line = strings.TrimSpace(line) if len(line) >= 9 && strings.EqualFold(line[:8], "endpoint") { _, val, ok := strings.Cut(line, "=") if !ok { _, val, ok = strings.Cut(line, ":") } if ok { return strings.TrimSpace(val) } } } } if i := strings.Index(proxy, "://"); i >= 0 { proxy = proxy[i+3:] } if at := strings.LastIndex(proxy, "@"); at >= 0 { proxy = proxy[at+1:] } if i := strings.IndexAny(proxy, "/?#"); i >= 0 { proxy = proxy[:i] } return proxy } // SetActive switches the active profile name. func (c *Config) SetActive(name string) error { name = strings.TrimSpace(name) for _, p := range c.Profiles { if p.Name == name { c.Active = name return nil } } return fmt.Errorf("profile %q not found", name) } // UpsertProfile creates or updates a profile by name. func (c *Config) UpsertProfile(name, proxy string) error { return c.UpsertProfileWithProtocol(name, proxy, "") } // UpsertProfileWithProtocol creates/updates a profile and optionally sets protocol. // The upserted profile becomes Active. func (c *Config) UpsertProfileWithProtocol(name, proxy string, proto Protocol) error { return c.upsertProfile(name, proxy, proto, true) } // UpsertProfileKeepActive creates/updates a profile without changing Active. func (c *Config) UpsertProfileKeepActive(name, proxy string, proto Protocol) error { return c.upsertProfile(name, proxy, proto, false) } func (c *Config) upsertProfile(name, proxy string, proto Protocol, activate bool) error { name = strings.TrimSpace(name) if name == "" { return fmt.Errorf("имя профиля пустое") } proxy = strings.TrimSpace(proxy) if proto == "" { proto = DetectProtocol(proxy) } if proto == "" { proto = ProtocolNaive } for i := range c.Profiles { if c.Profiles[i].Name == name { c.Profiles[i].Proxy = proxy c.Profiles[i].Protocol = proto if len(c.Profiles[i].Listen) == 0 { c.Profiles[i].Listen = defaultListen() } if activate { c.Active = name } return nil } } c.Profiles = append(c.Profiles, Profile{ Name: name, Protocol: proto, Proxy: proxy, Listen: defaultListen(), }) if activate { c.Active = name } return nil } // RenameProfile renames a profile and keeps active pointer in sync. func (c *Config) RenameProfile(oldName, newName string) error { oldName = strings.TrimSpace(oldName) newName = strings.TrimSpace(newName) if oldName == "" || newName == "" { return fmt.Errorf("имя профиля пустое") } if oldName == newName { return nil } for _, p := range c.Profiles { if p.Name == newName { return fmt.Errorf("профиль %q уже существует", newName) } } for i := range c.Profiles { if c.Profiles[i].Name == oldName { c.Profiles[i].Name = newName if c.Active == oldName { c.Active = newName } return nil } } return fmt.Errorf("profile %q not found", oldName) } // DeleteProfile removes a profile. Keeps at least one profile. func (c *Config) DeleteProfile(name string) error { name = strings.TrimSpace(name) if len(c.Profiles) <= 1 { return fmt.Errorf("нельзя удалить последний профиль") } idx := -1 for i, p := range c.Profiles { if p.Name == name { idx = i break } } if idx < 0 { return fmt.Errorf("profile %q not found", name) } c.Profiles = append(c.Profiles[:idx], c.Profiles[idx+1:]...) if c.Active == name { c.Active = c.Profiles[0].Name } return nil } func defaultListen() []string { return []string{ "socks://127.0.0.1:1080", "http://127.0.0.1:1081", } }