package hysteria2 import ( "fmt" "net/url" "strings" ) // NormalizeShareLink accepts hysteria2:// or hy2:// share links (or already-normalized URI). // Returns a canonical hysteria2:// URI suitable for the official client's `server` field. func NormalizeShareLink(raw string) (string, string, error) { raw = strings.TrimSpace(raw) if raw == "" { return "", "", fmt.Errorf("empty hysteria2 URI") } remark := "" if i := strings.IndexByte(raw, '#'); i >= 0 { remark = strings.TrimSpace(raw[i+1:]) if u, err := url.QueryUnescape(remark); err == nil { remark = u } raw = raw[:i] } lower := strings.ToLower(raw) switch { case strings.HasPrefix(lower, "hy2://"): raw = "hysteria2://" + raw[len("hy2://"):] case strings.HasPrefix(lower, "hysteria2://"): // ok default: return "", "", fmt.Errorf("ожидалась ссылка hysteria2:// или hy2://") } u, err := url.Parse(raw) if err != nil { return "", "", fmt.Errorf("parse hysteria2 URI: %w", err) } if u.Host == "" { return "", "", fmt.Errorf("hysteria2 URI missing host") } if u.Scheme != "hysteria2" { u.Scheme = "hysteria2" } return u.String(), remark, nil } // Detect returns true if raw looks like a Hysteria 2 share link. func Detect(raw string) bool { lower := strings.ToLower(strings.TrimSpace(raw)) return strings.HasPrefix(lower, "hysteria2://") || strings.HasPrefix(lower, "hy2://") } // HostPort extracts host and a single port for reachability checks. func HostPort(shareURI string) (host, port string, err error) { normalized, _, err := NormalizeShareLink(shareURI) if err != nil { return "", "", err } u, err := url.Parse(normalized) if err != nil { return "", "", err } host = u.Hostname() port = u.Port() if port == "" { // Multi-port may be in Hostname() for weird parses; Host may be "ex.com:443,5000-6000" hostField := u.Host if h, p, ok := splitMultiPortHost(hostField); ok { return h, p, nil } port = "443" } else if strings.Contains(port, ",") || strings.Contains(port, "-") { port = firstPortToken(port) } if host == "" { return "", "", fmt.Errorf("нет хоста") } if port == "" { port = "443" } return host, port, nil } func splitMultiPortHost(hostField string) (host, port string, ok bool) { // example.com:443,5000-6000 i := strings.LastIndex(hostField, ":") if i < 0 { return "", "", false } host = hostField[:i] port = firstPortToken(hostField[i+1:]) if host == "" || port == "" { return "", "", false } return host, port, true } func firstPortToken(ports string) string { ports = strings.TrimSpace(ports) if ports == "" { return "" } part := ports if i := strings.IndexByte(ports, ','); i >= 0 { part = ports[:i] } if i := strings.IndexByte(part, '-'); i >= 0 { part = part[:i] } return strings.TrimSpace(part) }