package profile import ( "errors" "fmt" "net" "net/url" "regexp" "strconv" "strings" ) type Mode string const ( SingleMode Mode = "single" AdvancedMode Mode = "advanced" ) type Profile struct { Name string `json:"name"` Mode Mode `json:"mode"` SingleURL string `json:"singleUrl,omitempty"` HTTPURL string `json:"httpUrl,omitempty"` HTTPSURL string `json:"httpsUrl,omitempty"` AllURL string `json:"allUrl,omitempty"` Bypass []string `json:"bypass,omitempty"` } type State struct { Profiles []Profile `json:"profiles"` ActiveProfile string `json:"activeProfile,omitempty"` } type Store interface { Load() (State, error) Mutate(func(State) (State, error)) (State, error) } func ValidateAndNormalize(input Profile) (Profile, error) { p := input p.Name = strings.TrimSpace(p.Name) if p.Name == "" { return Profile{}, errors.New("Profile name is required") } if p.Mode == "" { p.Mode = SingleMode } switch p.Mode { case SingleMode: value, err := validateURL("Single proxy URL", p.SingleURL) if err != nil { return Profile{}, err } p.SingleURL = value p.HTTPURL, p.HTTPSURL, p.AllURL = value, value, value case AdvancedMode: if p.HTTPURL == "" && p.HTTPSURL == "" && p.AllURL == "" && strings.TrimSpace(p.SingleURL) != "" { value, err := validateURL("Single proxy URL", p.SingleURL) if err != nil { return Profile{}, err } p.SingleURL = value p.HTTPURL, p.HTTPSURL, p.AllURL = value, value, value } var err error if p.HTTPURL, err = validateOptionalURL("HTTP proxy URL", p.HTTPURL); err != nil { return Profile{}, err } if p.HTTPSURL, err = validateOptionalURL("HTTPS proxy URL", p.HTTPSURL); err != nil { return Profile{}, err } if p.AllURL, err = validateOptionalURL("ALL/SOCKS proxy URL", p.AllURL); err != nil { return Profile{}, err } if p.HTTPURL == "" && p.HTTPSURL == "" && p.AllURL == "" { return Profile{}, errors.New("at least one advanced proxy URL is required") } default: return Profile{}, errors.New("proxy mode must be single or advanced") } bypass, err := normalizeBypass(p.Bypass) if err != nil { return Profile{}, err } p.Bypass = bypass return p, nil } func validateOptionalURL(field, value string) (string, error) { if strings.TrimSpace(value) == "" { return "", nil } return validateURL(field, value) } func validateURL(field, value string) (string, error) { value = strings.TrimSpace(value) if value == "" { return "", fmt.Errorf("%s is required", field) } u, err := url.Parse(value) if err != nil { return "", fmt.Errorf("%s is malformed", field) } switch strings.ToLower(u.Scheme) { case "http", "https", "socks5", "socks5h": default: return "", fmt.Errorf("%s must use http, https, socks5, or socks5h", field) } if u.Hostname() == "" { return "", fmt.Errorf("%s must include a host", field) } if u.Path != "" || u.RawQuery != "" || u.Fragment != "" { return "", fmt.Errorf("%s must not include a path, query, or fragment", field) } port := u.Port() if port == "" { return "", fmt.Errorf("%s must include a port", field) } n, err := strconv.Atoi(port) if err != nil || n < 1 || n > 65535 { return "", fmt.Errorf("%s port must be between 1 and 65535", field) } if net.ParseIP(u.Hostname()) == nil && (looksLikeIPv4(u.Hostname()) || !validHostname(u.Hostname())) { return "", fmt.Errorf("%s contains an invalid host", field) } u.Scheme = strings.ToLower(u.Scheme) return u.String(), nil } func normalizeBypass(values []string) ([]string, error) { result := make([]string, 0, len(values)) seen := make(map[string]bool) for _, raw := range values { for _, item := range strings.FieldsFunc(raw, func(r rune) bool { return r == ',' || r == '\n' }) { value, err := normalizeBypassEntry(item) if err != nil { return nil, err } if !seen[value] { seen[value] = true result = append(result, value) } } } return result, nil } func normalizeBypassEntry(raw string) (string, error) { value := strings.TrimSpace(raw) if value == "" { return "", errors.New("Bypass entries cannot be empty") } if _, network, err := net.ParseCIDR(value); err == nil { return network.String(), nil } if ip := net.ParseIP(value); ip != nil { return ip.String(), nil } suffix := strings.HasPrefix(value, ".") host := strings.ToLower(strings.TrimSuffix(strings.TrimPrefix(value, "."), ".")) if looksLikeIPv4(host) || !validHostname(host) { return "", errors.New("Bypass entry must be a hostname, domain suffix, IP address, or CIDR range") } if suffix { return "." + host, nil } return host, nil } var credentialPattern = regexp.MustCompile(`(?i)(https?|socks5h?)://([^/@\s:]+):([^@\s]*)@`) // RedactText removes URL passwords from diagnostic text without requiring the // surrounding text to be a valid URL. func RedactText(value string) string { return credentialPattern.ReplaceAllString(value, `${1}://${2}:REDACTED@`) } func ObscureURL(value string) string { u, err := url.Parse(value) if err != nil || u.User == nil { return RedactText(value) } if _, present := u.User.Password(); !present { return value } u.User = url.UserPassword(u.User.Username(), "REDACTED") return u.String() } func looksLikeIPv4(host string) bool { parts := strings.Split(host, ".") if len(parts) != 4 { return false } for _, part := range parts { if part == "" { return false } for _, r := range part { if r < '0' || r > '9' { return false } } } return true } func validHostname(host string) bool { if len(host) == 0 || len(host) > 253 || strings.HasPrefix(host, ".") || strings.HasSuffix(host, ".") { return false } for _, label := range strings.Split(host, ".") { if len(label) == 0 || len(label) > 63 || label[0] == '-' || label[len(label)-1] == '-' { return false } for _, r := range label { if !((r >= 'a' && r <= 'z') || (r >= 'A' && r <= 'Z') || (r >= '0' && r <= '9') || r == '-') { return false } } } return true }