feat: add proxy labels and default profiles
This commit is contained in:
@@ -68,6 +68,27 @@ func DefaultPath() (string, error) {
|
||||
return defaultPathForExecutable(executable)
|
||||
}
|
||||
|
||||
// DefaultInstanceUserDataRoot returns the portable root for new browser
|
||||
// profiles. It resolves the actual executable location and never uses cwd.
|
||||
func DefaultInstanceUserDataRoot() (string, error) {
|
||||
executable, err := os.Executable()
|
||||
if err != nil {
|
||||
return "", fmt.Errorf("%w: resolve executable", ErrDefaultConfigUnavailable)
|
||||
}
|
||||
return defaultInstanceUserDataRootForExecutable(executable)
|
||||
}
|
||||
|
||||
// DefaultInstanceUserDataDir returns a unique-instance profile path below the
|
||||
// executable-side user_data_dirs root. It only computes a path; it does not
|
||||
// create directories or validate their write permissions.
|
||||
func DefaultInstanceUserDataDir(instanceID string) (string, error) {
|
||||
root, err := DefaultInstanceUserDataRoot()
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
return defaultInstanceUserDataDir(root, instanceID)
|
||||
}
|
||||
|
||||
// LegacyPath is the pre-portable configuration location. It is read only when
|
||||
// the executable-side config does not exist yet.
|
||||
func LegacyPath() (string, error) {
|
||||
@@ -107,6 +128,23 @@ func defaultPathForExecutable(executable string) (string, error) {
|
||||
return filepath.Join(filepath.Dir(filepath.Clean(path)), "config.json"), nil
|
||||
}
|
||||
|
||||
func defaultInstanceUserDataRootForExecutable(executable string) (string, error) {
|
||||
path := strings.TrimSpace(executable)
|
||||
if path == "" || !filepath.IsAbs(path) {
|
||||
return "", fmt.Errorf("%w: executable path must be absolute", ErrDefaultConfigUnavailable)
|
||||
}
|
||||
return filepath.Join(filepath.Dir(filepath.Clean(path)), "user_data_dirs"), nil
|
||||
}
|
||||
|
||||
func defaultInstanceUserDataDir(root, instanceID string) (string, error) {
|
||||
root = strings.TrimSpace(root)
|
||||
instanceID = strings.TrimSpace(instanceID)
|
||||
if root == "" || !filepath.IsAbs(root) || instanceID == "" || filepath.Base(instanceID) != instanceID || instanceID == "." {
|
||||
return "", fmt.Errorf("%w: invalid default instance directory", ErrDefaultConfigUnavailable)
|
||||
}
|
||||
return filepath.Join(filepath.Clean(root), instanceID), nil
|
||||
}
|
||||
|
||||
func openDefaultAt(target, legacy string) (*Store, error) {
|
||||
store, targetExists, err := defaultStore(target)
|
||||
if err != nil || targetExists {
|
||||
@@ -303,28 +341,35 @@ func normalizeProxyConfig(value *File) error {
|
||||
return errors.New("config is required")
|
||||
}
|
||||
byID := make(map[string]ProxyProfile, len(value.Proxies))
|
||||
names := make(map[string]struct{}, len(value.Proxies))
|
||||
names := make([]string, 0, len(value.Proxies))
|
||||
servers := make(map[string]struct{}, len(value.Proxies))
|
||||
for i := range value.Proxies {
|
||||
profile := &value.Proxies[i]
|
||||
profile.ID = strings.TrimSpace(profile.ID)
|
||||
profile.Name = strings.TrimSpace(profile.Name)
|
||||
if profile.ID == "" || profile.Name == "" {
|
||||
name, err := domain.NormalizeProxyName(profile.Name)
|
||||
if profile.ID == "" || err != nil {
|
||||
return errors.New("invalid proxy configuration")
|
||||
}
|
||||
profile.Name = name
|
||||
if _, exists := byID[profile.ID]; exists {
|
||||
return errors.New("duplicate proxy configuration")
|
||||
}
|
||||
nameKey := strings.ToLower(profile.Name)
|
||||
if _, exists := names[nameKey]; exists {
|
||||
return errors.New("duplicate proxy configuration")
|
||||
for _, existing := range names {
|
||||
if strings.EqualFold(existing, profile.Name) {
|
||||
return errors.New("duplicate proxy configuration")
|
||||
}
|
||||
}
|
||||
server, err := domain.NormalizeProxyServer(profile.Server)
|
||||
if err != nil || server == "" {
|
||||
return errors.New("invalid proxy configuration")
|
||||
}
|
||||
if _, exists := servers[server]; exists {
|
||||
return errors.New("duplicate proxy configuration")
|
||||
}
|
||||
profile.Server = server
|
||||
byID[profile.ID] = *profile
|
||||
names[nameKey] = struct{}{}
|
||||
names = append(names, profile.Name)
|
||||
servers[server] = struct{}{}
|
||||
}
|
||||
|
||||
legacyByServer := make(map[string]string, len(value.Proxies))
|
||||
@@ -351,10 +396,12 @@ func normalizeProxyConfig(value *File) error {
|
||||
proxyID := legacyByServer[server]
|
||||
if proxyID == "" {
|
||||
proxyID = nextLegacyProxyID(byID)
|
||||
profile := ProxyProfile{ID: proxyID, Name: "导入代理 " + strconv.Itoa(len(value.Proxies)+1), Server: server}
|
||||
profile := ProxyProfile{ID: proxyID, Name: nextLegacyProxyName(names), Server: server}
|
||||
value.Proxies = append(value.Proxies, profile)
|
||||
byID[proxyID] = profile
|
||||
legacyByServer[server] = proxyID
|
||||
names = append(names, profile.Name)
|
||||
servers[server] = struct{}{}
|
||||
}
|
||||
instance.ProxyID = proxyID
|
||||
instance.Launch.ProxyServer = ""
|
||||
@@ -371,6 +418,22 @@ func nextLegacyProxyID(existing map[string]ProxyProfile) string {
|
||||
}
|
||||
}
|
||||
|
||||
func nextLegacyProxyName(existing []string) string {
|
||||
for index := 1; ; index++ {
|
||||
candidate := "导入代理 " + strconv.Itoa(index)
|
||||
duplicate := false
|
||||
for _, name := range existing {
|
||||
if strings.EqualFold(name, candidate) {
|
||||
duplicate = true
|
||||
break
|
||||
}
|
||||
}
|
||||
if !duplicate {
|
||||
return candidate
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func replace(target, temp string) error {
|
||||
backup := target + ".bak"
|
||||
_, statErr := os.Stat(target)
|
||||
|
||||
@@ -112,6 +112,28 @@ func TestDefaultPathForExecutableUsesExecutableDirectory(t *testing.T) {
|
||||
}
|
||||
}
|
||||
|
||||
func TestDefaultInstanceUserDataDirectoryUsesExecutableDirectoryAndStableID(t *testing.T) {
|
||||
executable := filepath.Join(t.TempDir(), "published", "chub.exe")
|
||||
root, err := defaultInstanceUserDataRootForExecutable(executable)
|
||||
if err != nil || root != filepath.Join(filepath.Dir(executable), "user_data_dirs") {
|
||||
t.Fatalf("defaultInstanceUserDataRootForExecutable() = %q, %v", root, err)
|
||||
}
|
||||
first, err := defaultInstanceUserDataDir(root, "instance-one")
|
||||
if err != nil || first != filepath.Join(root, "instance-one") {
|
||||
t.Fatalf("defaultInstanceUserDataDir() = %q, %v", first, err)
|
||||
}
|
||||
second, err := defaultInstanceUserDataDir(root, "instance-two")
|
||||
if err != nil || second == first {
|
||||
t.Fatalf("distinct instance directory = %q, %v", second, err)
|
||||
}
|
||||
if _, err := defaultInstanceUserDataRootForExecutable("chub.exe"); !errors.Is(err, ErrDefaultConfigUnavailable) {
|
||||
t.Fatalf("relative executable root error = %v", err)
|
||||
}
|
||||
if _, err := defaultInstanceUserDataDir(root, `..\other`); !errors.Is(err, ErrDefaultConfigUnavailable) {
|
||||
t.Fatalf("unsafe instance id error = %v", err)
|
||||
}
|
||||
}
|
||||
|
||||
func TestOpenDefaultAtMigratesValidLegacyWithoutRemovingIt(t *testing.T) {
|
||||
directory := t.TempDir()
|
||||
target := filepath.Join(directory, "portable", "config.json")
|
||||
@@ -249,6 +271,23 @@ func TestStoreRoundTripsReferencedProxyWithoutCredentials(t *testing.T) {
|
||||
}
|
||||
}
|
||||
|
||||
func TestStoreRejectsBlankOrDuplicateProxyNamesAndServers(t *testing.T) {
|
||||
store, err := New(filepath.Join(t.TempDir(), "config.json"))
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
for _, proxies := range [][]ProxyProfile{
|
||||
{{ID: "blank", Name: " ", Server: "http://127.0.0.1:8080"}},
|
||||
{{ID: "one", Name: "新加坡", Server: "http://127.0.0.1:8080"}, {ID: "two", Name: "新加坡", Server: "http://127.0.0.1:8081"}},
|
||||
{{ID: "one", Name: "Singapore", Server: "http://127.0.0.1:8080"}, {ID: "two", Name: "sINGAPORE", Server: "http://127.0.0.1:8081"}},
|
||||
{{ID: "one", Name: "新加坡", Server: "http://127.0.0.1:8080"}, {ID: "two", Name: "东京", Server: "HTTP://127.0.0.1:8080"}},
|
||||
} {
|
||||
if err := store.Save(File{Proxies: proxies}); err == nil {
|
||||
t.Fatalf("unsafe proxy profiles persisted: %#v", proxies)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func TestStoreMigratesLegacyProxyAndRejectsUnsafeConfig(t *testing.T) {
|
||||
path := filepath.Join(t.TempDir(), "config.json")
|
||||
if err := os.WriteFile(path, []byte(`{"version":1,"settings":{},"instances":[{"id":"a","name":"运营","launch":{"Kind":"chrome","UserDataDir":"C:\\profiles\\a","ProxyServer":"http://127.0.0.1:8080"}}]}`), 0o600); err != nil {
|
||||
|
||||
Reference in New Issue
Block a user