package linknorm import ( "fmt" "net/url" "strings" "vpnclient/internal/config" "vpnclient/internal/protocols/awg" "vpnclient/internal/protocols/hysteria2" "vpnclient/internal/protocols/naive" "vpnclient/internal/protocols/xray" ) // LooksLikeSubscriptionURL reports whether raw is a plain http(s) URL without // userinfo — i.e. a subscription URL rather than a naive proxy share link // (which always carries user:pass@). Callers should route such input into the // subscription import flow instead of the proxy-URI parser. func LooksLikeSubscriptionURL(raw string) bool { raw = strings.TrimSpace(raw) lower := strings.ToLower(raw) if !strings.HasPrefix(lower, "http://") && !strings.HasPrefix(lower, "https://") { return false } u, err := url.Parse(raw) if err != nil || u.Host == "" { return false } return u.User == nil } // Normalize normalizes a share/proxy URI for the given protocol (or auto-detect). func Normalize(proto config.Protocol, raw string) (normalized string, detected config.Protocol, remark string, err error) { raw = strings.TrimSpace(raw) if raw == "" { return "", proto, "", fmt.Errorf("пустая ссылка") } if proto == "" { proto = config.DetectProtocol(raw) } switch proto { case config.ProtocolHysteria2: u, rem, err := hysteria2.NormalizeShareLink(raw) return u, config.ProtocolHysteria2, rem, err case config.ProtocolAWG: u, rem, err := awg.NormalizeShareLink(raw) return u, config.ProtocolAWG, rem, err case config.ProtocolVLESS, config.ProtocolVMess, config.ProtocolTrojan: u, rem, err := xray.NormalizeShareLink(raw) if err != nil { return "", proto, "", err } detected = xray.DetectProtocol(u) if detected == "" { detected = proto } return u, detected, rem, nil case config.ProtocolNaive, "": if awg.Detect(raw) { u, rem, err := awg.NormalizeShareLink(raw) return u, config.ProtocolAWG, rem, err } // Safety net: a bare http(s) URL without user:pass@ is a subscription // URL, not a naive proxy link — callers should have routed it to the // subscription import flow (see LooksLikeSubscriptionURL). if LooksLikeSubscriptionURL(raw) { return "", config.ProtocolNaive, "", fmt.Errorf("это ссылка подписки (нет логина и пароля) — вставьте её в поле «URL подписки» или сохраните ещё раз: импорт запустится автоматически") } if xray.Detect(raw) { u, rem, err := xray.NormalizeShareLink(raw) return u, xray.DetectProtocol(raw), rem, err } u, rem, err := naive.ParseShareLink(raw) if err != nil { if hysteria2.Detect(raw) { u2, rem2, err2 := hysteria2.NormalizeShareLink(raw) return u2, config.ProtocolHysteria2, rem2, err2 } return "", config.ProtocolNaive, "", err } return u, config.ProtocolNaive, rem, nil default: return "", proto, "", fmt.Errorf("unsupported protocol %q", proto) } }