Files
kettuRay/core/parser.go
2026-03-31 14:40:03 +03:00

178 lines
4.1 KiB
Go

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
}
}