178 lines
4.1 KiB
Go
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
|
|
}
|
|
}
|