Files
3x-ui/internal/util/common/url.go
T
Duxxie 380aff4d82 Add remote routing URL support (#6168)
* Add remote routing URL support

* Harden remote routing refresh

* fix(sub): harden remote routing fetch and accept Mihomo src rule flag

Remote routing bytes reach the YAML/JSON parsers from goroutines that run
outside Gin's recovery, so a parser panic on crafted input would take down
the whole panel. Contain it in fetch() (a panic now degrades to a failed
refresh that keeps the last-good value and releases the in-flight slot)
and start the refresh, cache-load and startup-warm goroutines through
common.GoRecover like the other background workers.

The route-graph validator only skipped a trailing no-resolve flag, so a
valid Mihomo rule like IP-CIDR,x,DIRECT,no-resolve,src was rejected as an
unknown target; skip both option flags.

Also deduplicate the HTTPS-source classification into
common.ParseRemoteRoutingURL so the save-time validator and the resolver
can never drift (internal/sub imports internal/web/service, so the copy
existed only to avoid the import cycle), move the test-only
mergeRemoteClashRulesYAML helper into the test file, and trim oversized
comment blocks.

---------

Co-authored-by: Duxxie <yelloduxx@users.noreply.github.com>
Co-authored-by: Sanaei <ho3ein.sanaei@gmail.com>
2026-08-18 16:02:11 +02:00

48 lines
1.4 KiB
Go

package common
import (
"errors"
"net/url"
"strings"
)
// EnsureURLScheme prepends https:// to a URL that carries no scheme, so
// subscription apps and browsers don't resolve it relative to the panel's own
// domain (e.g. "t.me/support" turning into "https://panel.example/t.me/support").
// Values with an explicit scheme (https://, tg://, mailto:, tel:) and empty
// strings pass through untouched.
func EnsureURLScheme(raw string) string {
trimmed := strings.TrimSpace(raw)
if trimmed == "" {
return ""
}
if strings.Contains(trimmed, "://") ||
strings.HasPrefix(trimmed, "mailto:") ||
strings.HasPrefix(trimmed, "tel:") {
return trimmed
}
return "https://" + trimmed
}
// ParseRemoteRoutingURL classifies a routing settings value: one single-line
// absolute HTTPS URL is a remote source (canonicalized); anything else is inline.
func ParseRemoteRoutingURL(raw string) (string, bool, error) {
trimmed := strings.TrimSpace(raw)
if trimmed == "" || strings.ContainsAny(trimmed, "\r\n") {
return "", false, nil
}
if !strings.HasPrefix(strings.ToLower(trimmed), "https://") {
return "", false, nil
}
u, err := url.Parse(trimmed)
if err != nil || u.Host == "" || u.Hostname() == "" {
return "", true, errors.New("must be an absolute HTTPS URL")
}
if u.User != nil {
return "", true, errors.New("must not contain URL credentials")
}
u.Scheme = "https"
u.Fragment = ""
return u.String(), true, nil
}