Route credential-less http(s) URLs pasted into the add-link field to subscription import (fixes remaining 'proxy URI missing username'). Extend geoflag with RU country names and city hints; live Remnawave names already carrying emoji flags are kept as-is. Add admin provisioning via configs/remnawave-api.json (GET by-username / POST users, 50 GB MONTH plan) and the «Выдать доступ» UI panel. Co-authored-by: Cursor <cursoragent@cursor.com>
86 lines
2.9 KiB
Go
86 lines
2.9 KiB
Go
package linknorm
|
|
|
|
import (
|
|
"fmt"
|
|
"net/url"
|
|
"strings"
|
|
|
|
"vpnclient/internal/config"
|
|
"vpnclient/internal/protocols/awg"
|
|
"vpnclient/internal/protocols/hysteria2"
|
|
"vpnclient/internal/protocols/naive"
|
|
"vpnclient/internal/protocols/xray"
|
|
)
|
|
|
|
// LooksLikeSubscriptionURL reports whether raw is a plain http(s) URL without
|
|
// userinfo — i.e. a subscription URL rather than a naive proxy share link
|
|
// (which always carries user:pass@). Callers should route such input into the
|
|
// subscription import flow instead of the proxy-URI parser.
|
|
func LooksLikeSubscriptionURL(raw string) bool {
|
|
raw = strings.TrimSpace(raw)
|
|
lower := strings.ToLower(raw)
|
|
if !strings.HasPrefix(lower, "http://") && !strings.HasPrefix(lower, "https://") {
|
|
return false
|
|
}
|
|
u, err := url.Parse(raw)
|
|
if err != nil || u.Host == "" {
|
|
return false
|
|
}
|
|
return u.User == nil
|
|
}
|
|
|
|
// Normalize normalizes a share/proxy URI for the given protocol (or auto-detect).
|
|
func Normalize(proto config.Protocol, raw string) (normalized string, detected config.Protocol, remark string, err error) {
|
|
raw = strings.TrimSpace(raw)
|
|
if raw == "" {
|
|
return "", proto, "", fmt.Errorf("пустая ссылка")
|
|
}
|
|
if proto == "" {
|
|
proto = config.DetectProtocol(raw)
|
|
}
|
|
switch proto {
|
|
case config.ProtocolHysteria2:
|
|
u, rem, err := hysteria2.NormalizeShareLink(raw)
|
|
return u, config.ProtocolHysteria2, rem, err
|
|
case config.ProtocolAWG:
|
|
u, rem, err := awg.NormalizeShareLink(raw)
|
|
return u, config.ProtocolAWG, rem, err
|
|
case config.ProtocolVLESS, config.ProtocolVMess, config.ProtocolTrojan:
|
|
u, rem, err := xray.NormalizeShareLink(raw)
|
|
if err != nil {
|
|
return "", proto, "", err
|
|
}
|
|
detected = xray.DetectProtocol(u)
|
|
if detected == "" {
|
|
detected = proto
|
|
}
|
|
return u, detected, rem, nil
|
|
case config.ProtocolNaive, "":
|
|
if awg.Detect(raw) {
|
|
u, rem, err := awg.NormalizeShareLink(raw)
|
|
return u, config.ProtocolAWG, rem, err
|
|
}
|
|
// Safety net: a bare http(s) URL without user:pass@ is a subscription
|
|
// URL, not a naive proxy link — callers should have routed it to the
|
|
// subscription import flow (see LooksLikeSubscriptionURL).
|
|
if LooksLikeSubscriptionURL(raw) {
|
|
return "", config.ProtocolNaive, "", fmt.Errorf("это ссылка подписки (нет логина и пароля) — вставьте её в поле «URL подписки» или сохраните ещё раз: импорт запустится автоматически")
|
|
}
|
|
if xray.Detect(raw) {
|
|
u, rem, err := xray.NormalizeShareLink(raw)
|
|
return u, xray.DetectProtocol(raw), rem, err
|
|
}
|
|
u, rem, err := naive.ParseShareLink(raw)
|
|
if err != nil {
|
|
if hysteria2.Detect(raw) {
|
|
u2, rem2, err2 := hysteria2.NormalizeShareLink(raw)
|
|
return u2, config.ProtocolHysteria2, rem2, err2
|
|
}
|
|
return "", config.ProtocolNaive, "", err
|
|
}
|
|
return u, config.ProtocolNaive, rem, nil
|
|
default:
|
|
return "", proto, "", fmt.Errorf("unsupported protocol %q", proto)
|
|
}
|
|
}
|