114 lines
2.8 KiB
Go
114 lines
2.8 KiB
Go
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)
|
|
}
|