package core import ( "fmt" "net/url" "strconv" "strings" ) // ParseLink parses a VPN link and returns a ProxyLink. // Supported protocols: trojan, vless. func ParseLink(link string) (*ProxyLink, error) { link = strings.TrimSpace(link) if link == "" { return nil, fmt.Errorf("link cannot be empty") } protocol, err := getProtocol(link) if err != nil { return nil, err } switch protocol { case "trojan", "vless": return parseTrojanOrVless(link, protocol) default: return nil, fmt.Errorf("unsupported protocol: %s", protocol) } } // TryParseLink attempts to parse a VPN link, returning nil on failure. func TryParseLink(link string) *ProxyLink { result, err := ParseLink(link) if err != nil { return nil } return result } func getProtocol(link string) (string, error) { idx := strings.Index(link, "://") if idx < 0 { return "", fmt.Errorf("invalid link format: scheme (protocol://) is missing") } return strings.ToLower(link[:idx]), nil } // parseTrojanOrVless parses links of format: protocol://credential@host:port?params#remark func parseTrojanOrVless(link, protocol string) (*ProxyLink, error) { result := &ProxyLink{ Protocol: protocol, Security: "none", Fingerprint: "chrome", Transport: "tcp", } // Remove scheme remainder := link[len(protocol)+3:] // Extract fragment (#remark) if fragmentIdx := strings.LastIndex(remainder, "#"); fragmentIdx >= 0 { decoded, err := url.PathUnescape(remainder[fragmentIdx+1:]) if err != nil { result.Remark = remainder[fragmentIdx+1:] } else { result.Remark = decoded } remainder = remainder[:fragmentIdx] } // Extract query string (?params) var queryParams url.Values if queryIdx := strings.Index(remainder, "?"); queryIdx >= 0 { var err error queryParams, err = url.ParseQuery(remainder[queryIdx+1:]) if err != nil { queryParams = url.Values{} } remainder = remainder[:queryIdx] } else { queryParams = url.Values{} } // Extract credential@host:port atIdx := strings.Index(remainder, "@") if atIdx < 0 { return nil, fmt.Errorf("invalid format: missing credential@host:port") } decoded, err := url.PathUnescape(remainder[:atIdx]) if err != nil { result.Credential = remainder[:atIdx] } else { result.Credential = decoded } hostPort := remainder[atIdx+1:] // Parse host:port if err := parseHostPort(hostPort, result); err != nil { return nil, err } // Parse query parameters parseQueryParams(queryParams, result) return result, nil } func parseHostPort(hostPort string, result *ProxyLink) error { var lastColon int if strings.HasPrefix(hostPort, "[") { // IPv6: [::1]:443 bracketEnd := strings.Index(hostPort, "]") if bracketEnd < 0 { return fmt.Errorf("invalid IPv6 address: missing closing bracket") } result.Address = hostPort[1:bracketEnd] lastColon = strings.Index(hostPort[bracketEnd:], ":") if lastColon >= 0 { lastColon += bracketEnd } } else { lastColon = strings.LastIndex(hostPort, ":") if lastColon < 0 { return fmt.Errorf("invalid format: missing port") } result.Address = hostPort[:lastColon] } if lastColon < 0 || lastColon >= len(hostPort)-1 { return fmt.Errorf("invalid format: missing port") } portStr := hostPort[lastColon+1:] port, err := strconv.Atoi(portStr) if err != nil || port < 1 || port > 65535 { return fmt.Errorf("invalid port: %s", portStr) } result.Port = port return nil } func parseQueryParams(params url.Values, result *ProxyLink) { if v := params.Get("security"); v != "" { result.Security = strings.ToLower(v) } if v := params.Get("sni"); v != "" { result.Sni = v } if v := params.Get("fp"); v != "" { result.Fingerprint = v } if v := params.Get("pbk"); v != "" { result.PublicKey = v } if v := params.Get("sid"); v != "" { result.ShortId = v } if v := params.Get("type"); v != "" { result.Transport = strings.ToLower(v) } if v := params.Get("serviceName"); v != "" { result.ServiceName = v } if v := params.Get("path"); v != "" { result.Path = v } if v := params.Get("host"); v != "" { result.Host = v } if v := params.Get("flow"); v != "" { result.Flow = v } }