Release 3.9.0+2: tolerant subscription import, full Remnawave server list, subscription info card (expiry / traffic / devices).
Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
@@ -87,7 +87,7 @@ func ParseInput(raw string) (base, shortUUID string, ok bool) {
|
||||
|
||||
// Fetch downloads configs via Remnawave public sub URL and, if a token is set,
|
||||
// authenticated subscription/user endpoints. Parsed items reuse subscription.ParseBody.
|
||||
func Fetch(ctx context.Context, s Settings) ([]subscription.Item, string, error) {
|
||||
func Fetch(ctx context.Context, s Settings) (*subscription.Result, string, error) {
|
||||
s.BaseURL = NormalizeBase(s.BaseURL)
|
||||
s.ShortUUID = strings.TrimSpace(s.ShortUUID)
|
||||
s.Token = strings.TrimSpace(s.Token)
|
||||
@@ -105,19 +105,19 @@ func Fetch(ctx context.Context, s Settings) ([]subscription.Item, string, error)
|
||||
var lastErr error
|
||||
|
||||
// 1) Public subscription (usual end-user path — no token).
|
||||
if items, err := fetchAndParse(ctx, client, s, pub, false); err == nil && len(items) > 0 {
|
||||
return items, pub, nil
|
||||
if res, err := fetchAndParse(ctx, client, s, pub, false); err == nil && res != nil && len(res.Items) > 0 {
|
||||
return res, pub, nil
|
||||
} else if err != nil {
|
||||
lastErr = err
|
||||
}
|
||||
|
||||
// 2) With API token: resolve subscriptionUrl / raw payload.
|
||||
if s.Token != "" {
|
||||
if items, used, err := fetchWithToken(ctx, client, s); err == nil && len(items) > 0 {
|
||||
if res, used, err := fetchWithToken(ctx, client, s); err == nil && res != nil && len(res.Items) > 0 {
|
||||
if used == "" {
|
||||
used = pub
|
||||
}
|
||||
return items, used, nil
|
||||
return res, used, nil
|
||||
} else if err != nil {
|
||||
lastErr = err
|
||||
}
|
||||
@@ -129,7 +129,7 @@ func Fetch(ctx context.Context, s Settings) ([]subscription.Item, string, error)
|
||||
return nil, "", fmt.Errorf("Remnawave: нет поддерживаемых ссылок (проверьте short UUID / токен / правила ответа подписки)")
|
||||
}
|
||||
|
||||
func fetchWithToken(ctx context.Context, client *http.Client, s Settings) ([]subscription.Item, string, error) {
|
||||
func fetchWithToken(ctx context.Context, client *http.Client, s Settings) (*subscription.Result, string, error) {
|
||||
base := s.BaseURL
|
||||
short := s.ShortUUID
|
||||
|
||||
@@ -142,18 +142,18 @@ func fetchWithToken(ctx context.Context, client *http.Client, s Settings) ([]sub
|
||||
}
|
||||
var lastErr error
|
||||
for _, endpoint := range candidates {
|
||||
body, err := doGET(ctx, client, s, endpoint, true)
|
||||
body, _, err := doGET(ctx, client, s, endpoint, true)
|
||||
if err != nil {
|
||||
lastErr = err
|
||||
continue
|
||||
}
|
||||
if items, err := parseRemnawaveBody(body); err == nil && len(items) > 0 {
|
||||
return items, endpoint, nil
|
||||
if res, err := parseRemnawaveBody(body); err == nil && res != nil && len(res.Items) > 0 {
|
||||
return res, endpoint, nil
|
||||
}
|
||||
if subURL := extractSubscriptionURL(body); subURL != "" {
|
||||
items, err := subscription.Fetch(ctx, subURL)
|
||||
if err == nil && len(items) > 0 {
|
||||
return items, subURL, nil
|
||||
res, err := subscription.Fetch(ctx, subURL)
|
||||
if err == nil && res != nil && len(res.Items) > 0 {
|
||||
return res, subURL, nil
|
||||
}
|
||||
if err != nil {
|
||||
lastErr = err
|
||||
@@ -166,18 +166,22 @@ func fetchWithToken(ctx context.Context, client *http.Client, s Settings) ([]sub
|
||||
return nil, "", fmt.Errorf("Remnawave API: не удалось получить конфиги")
|
||||
}
|
||||
|
||||
func fetchAndParse(ctx context.Context, client *http.Client, s Settings, endpoint string, auth bool) ([]subscription.Item, error) {
|
||||
body, err := doGET(ctx, client, s, endpoint, auth)
|
||||
func fetchAndParse(ctx context.Context, client *http.Client, s Settings, endpoint string, auth bool) (*subscription.Result, error) {
|
||||
body, header, err := doGET(ctx, client, s, endpoint, auth)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return parseRemnawaveBody(body)
|
||||
res, err := parseRemnawaveBody(body)
|
||||
if res != nil && res.Info == nil {
|
||||
res.Info = subscription.ParseUserInfoHeader(header.Get("Subscription-Userinfo"))
|
||||
}
|
||||
return res, err
|
||||
}
|
||||
|
||||
func doGET(ctx context.Context, client *http.Client, s Settings, endpoint string, auth bool) (string, error) {
|
||||
func doGET(ctx context.Context, client *http.Client, s Settings, endpoint string, auth bool) (string, http.Header, error) {
|
||||
req, err := http.NewRequestWithContext(ctx, http.MethodGet, endpoint, nil)
|
||||
if err != nil {
|
||||
return "", err
|
||||
return "", nil, err
|
||||
}
|
||||
req.Header.Set("User-Agent", "Navis/3.9 (Remnawave)")
|
||||
req.Header.Set("Accept", "*/*")
|
||||
@@ -196,21 +200,21 @@ func doGET(ctx context.Context, client *http.Client, s Settings, endpoint string
|
||||
|
||||
resp, err := client.Do(req)
|
||||
if err != nil {
|
||||
return "", fmt.Errorf("Remnawave запрос: %w", err)
|
||||
return "", nil, fmt.Errorf("Remnawave запрос: %w", err)
|
||||
}
|
||||
defer resp.Body.Close()
|
||||
raw, err := io.ReadAll(io.LimitReader(resp.Body, 8<<20))
|
||||
if err != nil {
|
||||
return "", err
|
||||
return "", nil, err
|
||||
}
|
||||
if resp.StatusCode < 200 || resp.StatusCode >= 300 {
|
||||
msg := strings.TrimSpace(string(raw))
|
||||
if len(msg) > 180 {
|
||||
msg = msg[:180] + "…"
|
||||
}
|
||||
return "", fmt.Errorf("Remnawave HTTP %d: %s", resp.StatusCode, msg)
|
||||
return "", nil, fmt.Errorf("Remnawave HTTP %d: %s", resp.StatusCode, msg)
|
||||
}
|
||||
return string(raw), nil
|
||||
return string(raw), resp.Header, nil
|
||||
}
|
||||
|
||||
func shouldSendProxyHeaders(u *url.URL) bool {
|
||||
@@ -228,24 +232,24 @@ func shouldSendProxyHeaders(u *url.URL) bool {
|
||||
return ip != nil && (ip.IsLoopback() || ip.IsPrivate())
|
||||
}
|
||||
|
||||
func parseRemnawaveBody(body string) ([]subscription.Item, error) {
|
||||
func parseRemnawaveBody(body string) (*subscription.Result, error) {
|
||||
body = strings.TrimSpace(body)
|
||||
if body == "" {
|
||||
return nil, fmt.Errorf("пустой ответ Remnawave")
|
||||
}
|
||||
// Try JSON envelope / raw DTO first, then classic subscription parse.
|
||||
if strings.HasPrefix(body, "{") || strings.HasPrefix(body, "[") {
|
||||
if items, err := subscription.ParseBody(flattenJSONLinks(body)); err == nil && len(items) > 0 {
|
||||
return items, nil
|
||||
if res, err := subscription.ParseBodyDetailed(flattenJSONLinks(body)); err == nil && len(res.Items) > 0 {
|
||||
return res, nil
|
||||
}
|
||||
// Also try parsing the whole JSON as if links were string values only.
|
||||
if flat := extractLinkStrings(body); flat != "" {
|
||||
if items, err := subscription.ParseBody(flat); err == nil && len(items) > 0 {
|
||||
return items, nil
|
||||
if res, err := subscription.ParseBodyDetailed(flat); err == nil && len(res.Items) > 0 {
|
||||
return res, nil
|
||||
}
|
||||
}
|
||||
}
|
||||
return subscription.ParseBody(body)
|
||||
return subscription.ParseBodyDetailed(body)
|
||||
}
|
||||
|
||||
func flattenJSONLinks(body string) string {
|
||||
|
||||
Reference in New Issue
Block a user