mirror of
https://github.com/MHSanaei/3x-ui.git
synced 2026-11-02 03:05:02 +03:00
661a169045
Remote routing sources are fetched documents the panel re-renders on every subscription request. A Clash YAML document stays small through aliases, which decode to one shared value but are written out in full, so it now has to fit a 4 MiB written-size budget and 16 levels of nesting when it is accepted (and when a persisted copy is loaded). A JSON routing profile could repeat route groups, each copy repeating every site list; a group now counts once, since a repeat can never match.
205 lines
7.4 KiB
Go
205 lines
7.4 KiB
Go
package sub
|
|
|
|
import (
|
|
"encoding/base64"
|
|
"encoding/json"
|
|
"net/http"
|
|
"strings"
|
|
"testing"
|
|
)
|
|
|
|
func mustMarshal(t *testing.T, v any) string {
|
|
t.Helper()
|
|
data, err := json.Marshal(v)
|
|
if err != nil {
|
|
t.Fatalf("marshal: %v", err)
|
|
}
|
|
return string(data)
|
|
}
|
|
|
|
func b64Std(s string) string { return base64.StdEncoding.EncodeToString([]byte(s)) }
|
|
func b64URL(s string) string { return base64.RawURLEncoding.EncodeToString([]byte(s)) }
|
|
|
|
func fullRoutingPayload() map[string]any {
|
|
return map[string]any{
|
|
"Name": "RoscomVPN",
|
|
"DomainStrategy": "IPIfNonMatch",
|
|
"RemoteDNSDomain": "https://8.8.8.8/dns-query",
|
|
"RemoteDNSIP": "8.8.8.8",
|
|
"DomesticDNSDomain": "https://77.88.8.8/dns-query",
|
|
"DomesticDNSIP": "77.88.8.8",
|
|
"DnsHosts": map[string]any{"lknpd.nalog.ru": "213.24.64.181"},
|
|
"RouteOrder": "block-proxy-direct",
|
|
"DirectSites": []any{"geosite:category-ru", "geosite:private"},
|
|
"DirectIp": []any{"geoip:private"},
|
|
"ProxySites": []any{"geosite:youtube"},
|
|
"ProxyIp": []any{},
|
|
"BlockSites": []any{"geosite:category-ads"},
|
|
"BlockIp": []any{},
|
|
}
|
|
}
|
|
|
|
func TestParseJsonRoutingSpecMapsAllFields(t *testing.T) {
|
|
spec, remote, err := parseJsonRoutingSpec(mustMarshal(t, fullRoutingPayload()))
|
|
if err != nil || remote {
|
|
t.Fatalf("parse: err=%v remote=%v", err, remote)
|
|
}
|
|
want := jsonRoutingSpec{
|
|
DomainStrategy: "IPIfNonMatch",
|
|
RemoteDNSDomain: "https://8.8.8.8/dns-query",
|
|
RemoteDNSIP: "8.8.8.8",
|
|
DomesticDNSDomain: "https://77.88.8.8/dns-query",
|
|
DomesticDNSIP: "77.88.8.8",
|
|
DnsHosts: map[string]string{"lknpd.nalog.ru": "213.24.64.181"},
|
|
RouteOrder: []string{"block", "proxy", "direct"},
|
|
DirectSites: []string{"geosite:category-ru", "geosite:private"},
|
|
DirectIp: []string{"geoip:private"},
|
|
ProxySites: []string{"geosite:youtube"},
|
|
BlockSites: []string{"geosite:category-ads"},
|
|
}
|
|
if spec.DomainStrategy != want.DomainStrategy || spec.RemoteDNSIP != want.RemoteDNSIP ||
|
|
spec.DomesticDNSDomain != want.DomesticDNSDomain || len(spec.DnsHosts) != 1 || spec.DnsHosts["lknpd.nalog.ru"] != "213.24.64.181" ||
|
|
strings.Join(spec.RouteOrder, ",") != strings.Join(want.RouteOrder, ",") ||
|
|
strings.Join(spec.DirectSites, ",") != strings.Join(want.DirectSites, ",") ||
|
|
strings.Join(spec.DirectIp, ",") != strings.Join(want.DirectIp, ",") ||
|
|
strings.Join(spec.ProxySites, ",") != strings.Join(want.ProxySites, ",") ||
|
|
strings.Join(spec.BlockSites, ",") != strings.Join(want.BlockSites, ",") {
|
|
t.Fatalf("spec = %+v\nwant %+v", spec, want)
|
|
}
|
|
}
|
|
|
|
func TestParseJsonRoutingSpecPartialPayload(t *testing.T) {
|
|
spec, _, err := parseJsonRoutingSpec(`{"DirectSites":["geosite:private"],"DomainStrategy":"AsIs"}`)
|
|
if err != nil {
|
|
t.Fatalf("parse: %v", err)
|
|
}
|
|
if spec.DomainStrategy != "AsIs" || len(spec.DirectSites) != 1 || spec.DirectSites[0] != "geosite:private" {
|
|
t.Fatalf("spec = %+v", spec)
|
|
}
|
|
if len(spec.RouteOrder) != 0 || len(spec.DnsHosts) != 0 || spec.RemoteDNSIP != "" {
|
|
t.Fatalf("unset fields must stay zero: %+v", spec)
|
|
}
|
|
if spec.empty() {
|
|
t.Fatalf("empty() must report false when any field is set: %+v", spec)
|
|
}
|
|
}
|
|
|
|
func TestParseJsonRoutingSpecRouteOrderUnknownSegments(t *testing.T) {
|
|
spec, _, err := parseJsonRoutingSpec(`{"RouteOrder":"block-foo-direct"}`)
|
|
if err != nil {
|
|
t.Fatalf("parse: %v", err)
|
|
}
|
|
if strings.Join(spec.RouteOrder, ",") != "block,direct" {
|
|
t.Fatalf("RouteOrder = %v", spec.RouteOrder)
|
|
}
|
|
}
|
|
|
|
// Every group copy repeats the profile's whole site lists in the rendered routing, and a
|
|
// later copy can never match, so a remote profile repeating groups must not multiply them.
|
|
func TestParseJsonRoutingSpecRouteOrderKeepsEachGroupOnce(t *testing.T) {
|
|
spec, _, err := parseJsonRoutingSpec(`{"RouteOrder":"` + strings.Repeat("proxy-direct-block-", 500) + `proxy"}`)
|
|
if err != nil {
|
|
t.Fatalf("parse: %v", err)
|
|
}
|
|
if strings.Join(spec.RouteOrder, ",") != "proxy,direct,block" {
|
|
t.Fatalf("RouteOrder = %d segments, want proxy,direct,block once each", len(spec.RouteOrder))
|
|
}
|
|
}
|
|
|
|
func TestParseJsonRoutingSpecDeeplinks(t *testing.T) {
|
|
payload := mustMarshal(t, fullRoutingPayload())
|
|
cases := []string{
|
|
"happ://routing/onadd/" + b64Std(payload),
|
|
"incy://routing/onadd/" + b64Std(payload),
|
|
"happ://routing/onadd/" + b64URL(payload),
|
|
}
|
|
for _, raw := range cases {
|
|
spec, _, err := parseJsonRoutingSpec(raw)
|
|
if err != nil {
|
|
t.Fatalf("parse %q: %v", raw[:32], err)
|
|
}
|
|
if spec.DomainStrategy != "IPIfNonMatch" || len(spec.DirectSites) != 2 || spec.RouteOrder[1] != "proxy" {
|
|
t.Fatalf("spec from %q = %+v", raw[:32], spec)
|
|
}
|
|
}
|
|
}
|
|
|
|
func TestParseJsonRoutingSpecRejectsBadPayloads(t *testing.T) {
|
|
cases := []string{
|
|
"not json at all",
|
|
"[1,2,3]",
|
|
`{"DirectSites":"geosite:private"}`,
|
|
`{"DirectSites":["a",1]}`,
|
|
`{"DnsHosts":{"a":1}}`,
|
|
`{"DomainStrategy":5}`,
|
|
"happ://routing/onadd/!!!!not-base64!!!!",
|
|
}
|
|
for _, raw := range cases {
|
|
if _, _, err := parseJsonRoutingSpec(raw); err == nil {
|
|
t.Fatalf("payload %q was accepted", raw)
|
|
}
|
|
}
|
|
}
|
|
|
|
func TestParseJsonRoutingSpecEmpty(t *testing.T) {
|
|
for _, raw := range []string{"", " ", "\n"} {
|
|
spec, remote, err := parseJsonRoutingSpec(raw)
|
|
if err != nil || remote || !spec.empty() {
|
|
t.Fatalf("raw=%q spec=%+v remote=%v err=%v", raw, spec, remote, err)
|
|
}
|
|
}
|
|
}
|
|
|
|
func TestParseJsonRoutingSpecRemoteURL(t *testing.T) {
|
|
oldResolver := routingSourceResolver
|
|
t.Cleanup(func() { routingSourceResolver = oldResolver })
|
|
routingSourceResolver = newRemoteRoutingResolver(remoteRoutingTestClient(func(*http.Request) (*http.Response, error) {
|
|
return remoteRoutingResponse(200, mustMarshal(t, fullRoutingPayload())), nil
|
|
}), false)
|
|
|
|
const source = "https://example.com/DEFAULT.JSON"
|
|
primeRemoteRouting(t, routingSourceResolver, remoteRoutingJson, source)
|
|
spec, _, err := parseJsonRoutingSpec(source)
|
|
if err != nil {
|
|
t.Fatalf("parse: err=%v", err)
|
|
}
|
|
if spec.DomainStrategy != "IPIfNonMatch" || len(spec.BlockSites) != 1 {
|
|
t.Fatalf("spec = %+v", spec)
|
|
}
|
|
}
|
|
|
|
func TestParseJsonRoutingSpecRemoteUnavailable(t *testing.T) {
|
|
oldResolver := routingSourceResolver
|
|
t.Cleanup(func() { routingSourceResolver = oldResolver })
|
|
routingSourceResolver = newRemoteRoutingResolver(remoteRoutingTestClient(func(*http.Request) (*http.Response, error) {
|
|
return remoteRoutingResponse(200, "routing.help"), nil
|
|
}), false)
|
|
if _, _, err := parseJsonRoutingSpec("https://example.com/bad"); err == nil {
|
|
t.Fatal("unavailable remote source must error")
|
|
}
|
|
}
|
|
|
|
// normalizeHappRouting accepts happ://routing/add/ as a routing deeplink
|
|
// (remote_routing.go), so the baked-JSON parser has to accept it too.
|
|
func TestParseJsonRoutingSpecAcceptsAddDeeplink(t *testing.T) {
|
|
payload := mustMarshal(t, fullRoutingPayload())
|
|
for _, prefix := range []string{"happ://routing/onadd/", "happ://routing/add/", "incy://routing/onadd/"} {
|
|
t.Run(prefix, func(t *testing.T) {
|
|
if _, err := normalizeHappRouting([]byte(prefix + b64Std(payload))); err != nil &&
|
|
!strings.HasPrefix(prefix, "incy://") {
|
|
t.Fatalf("normalizeHappRouting rejects %s: %v", prefix, err)
|
|
}
|
|
spec, remote, err := parseJsonRoutingSpec(prefix + b64Std(payload))
|
|
if err != nil || remote {
|
|
t.Fatalf("parse %s: err=%v remote=%v", prefix, err, remote)
|
|
}
|
|
if spec.empty() {
|
|
t.Fatalf("parse %s: spec is empty, routing would not be baked", prefix)
|
|
}
|
|
if spec.DomainStrategy != "IPIfNonMatch" {
|
|
t.Fatalf("parse %s: DomainStrategy = %q", prefix, spec.DomainStrategy)
|
|
}
|
|
})
|
|
}
|
|
}
|