Skip to content

Commit 3e28eb0

Browse files
committed
Add listable
1 parent f6a7e93 commit 3e28eb0

7 files changed

Lines changed: 230 additions & 44 deletions

File tree

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
package duration
1+
package types
22

33
import (
44
"encoding/json"

infra/conf/cfgcommon/duration/duration_test.go renamed to infra/conf/cfgcommon/types/duration_test.go

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -1,20 +1,20 @@
1-
package duration_test
1+
package types_test
22

33
import (
44
"encoding/json"
55
"testing"
66
"time"
77

8-
"github.com/xtls/xray-core/infra/conf/cfgcommon/duration"
8+
"github.com/xtls/xray-core/infra/conf/cfgcommon/types"
99
)
1010

1111
type testWithDuration struct {
12-
Duration duration.Duration
12+
Duration types.Duration
1313
}
1414

1515
func TestDurationJSON(t *testing.T) {
1616
expected := &testWithDuration{
17-
Duration: duration.Duration(time.Hour),
17+
Duration: types.Duration(time.Hour),
1818
}
1919
data, err := json.Marshal(expected)
2020
if err != nil {
Lines changed: 37 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,37 @@
1+
package types
2+
3+
import (
4+
"encoding/json"
5+
"slices"
6+
"strings"
7+
)
8+
9+
// Listable allows a field to be unmarshalled from a single object or a list of objects.
10+
// If the json input is a single object, it will be stored as a slice with one element.
11+
// If the json input is null or empty, it will be nil.
12+
type Listable[T any] []T
13+
14+
func (l *Listable[T]) UnmarshalJSON(data []byte) error {
15+
var v T
16+
if len(data) != 0 && !slices.Equal(data, []byte("null")) && data[0] != '[' {
17+
if err := json.Unmarshal(data, &v); err == nil {
18+
*l = []T{v}
19+
return err
20+
}
21+
}
22+
return json.Unmarshal(data, (*[]T)(l))
23+
}
24+
25+
// ListableSimpleString is like Listable[string], but able to separate by `~`
26+
type ListableSimpleString []string
27+
28+
func (l *ListableSimpleString) UnmarshalJSON(data []byte) error {
29+
var v string
30+
if len(data) != 0 && !slices.Equal(data, []byte("null")) && data[0] != '[' {
31+
if err := json.Unmarshal(data, &v); err == nil {
32+
*l = strings.Split(v, "~")
33+
return nil
34+
}
35+
}
36+
return json.Unmarshal(data, (*[]string)(l))
37+
}
Lines changed: 149 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,149 @@
1+
package types_test
2+
3+
import (
4+
"encoding/json"
5+
"slices"
6+
"testing"
7+
8+
"github.com/xtls/xray-core/infra/conf/cfgcommon/types"
9+
)
10+
11+
type TestGroup[T any] struct {
12+
name string
13+
input string
14+
expected []T
15+
}
16+
17+
// intentionally to be so chaos
18+
var rawJson = `{
19+
"field":
20+
["value1",
21+
"value2", "value3"
22+
]
23+
}`
24+
25+
func TestListableUnmarshal(t *testing.T) {
26+
type TestStruct struct {
27+
Field types.Listable[string] `json:"field"`
28+
}
29+
30+
tests := []TestGroup[string]{
31+
{
32+
name: "SingleString",
33+
input: `{"field": "hello"}`,
34+
expected: []string{"hello"},
35+
},
36+
{
37+
name: "ArrayString",
38+
input: `{"field": ["value1", "value2", "value3"]}`,
39+
expected: []string{"value1", "value2", "value3"},
40+
},
41+
{
42+
name: "ComplexArray",
43+
input: rawJson,
44+
expected: []string{"value1", "value2", "value3"},
45+
},
46+
{
47+
name: "SingleStringWithSpace",
48+
input: `{"field": "hello" }`,
49+
expected: []string{"hello"},
50+
},
51+
{
52+
name: "ArrayWithSpace",
53+
input: `{"field": [ "a", "b" ] }`,
54+
expected: []string{"a", "b"},
55+
},
56+
{
57+
name: "Null",
58+
input: `{"field": null}`,
59+
expected: nil,
60+
},
61+
{
62+
name: "Missing (default)",
63+
input: `{}`,
64+
expected: nil,
65+
},
66+
}
67+
68+
for _, tt := range tests {
69+
t.Run(tt.name, func(t *testing.T) {
70+
var ts TestStruct
71+
err := json.Unmarshal([]byte(tt.input), &ts)
72+
if err != nil {
73+
t.Fatalf("Unmarshal failed: %v", err)
74+
}
75+
if !slices.Equal([]string(ts.Field), tt.expected) {
76+
t.Errorf("Expected %v, got %v", tt.expected, ts.Field)
77+
}
78+
})
79+
}
80+
}
81+
82+
func TestListableInt(t *testing.T) {
83+
tests := []TestGroup[int]{
84+
{
85+
name: "SingleInt",
86+
input: `123`,
87+
expected: []int{123},
88+
},
89+
{
90+
name: "ArrayInt",
91+
input: `[1, 2]`,
92+
expected: []int{1, 2},
93+
},
94+
{
95+
name: "Null",
96+
input: `null`,
97+
expected: nil,
98+
},
99+
}
100+
101+
for _, tt := range tests {
102+
t.Run(tt.name, func(t *testing.T) {
103+
var l types.Listable[int]
104+
err := json.Unmarshal([]byte(tt.input), &l)
105+
if err != nil {
106+
t.Fatalf("Unmarshal failed: %v", err)
107+
}
108+
if !slices.Equal([]int(l), tt.expected) {
109+
t.Errorf("Expected %v, got %v", tt.expected, l)
110+
}
111+
})
112+
}
113+
}
114+
115+
func TestListableSimpleString(t *testing.T) {
116+
type TestStruct struct {
117+
Field types.ListableSimpleString `json:"field"`
118+
}
119+
120+
tests := []TestGroup[string]{
121+
{
122+
name: "SingleString",
123+
input: `{"field": "singleValue"}`,
124+
expected: []string{"singleValue"},
125+
},
126+
{
127+
name: "ArrayString",
128+
input: `{"field": ["value1", "value2", "value3"]}`,
129+
expected: []string{"value1", "value2", "value3"},
130+
},
131+
{
132+
name: "WaveSplit",
133+
input: `{"field": "value1~value2~value3"}`,
134+
expected: []string{"value1", "value2", "value3"},
135+
},
136+
}
137+
for _, tt := range tests {
138+
t.Run(tt.name, func(t *testing.T) {
139+
var ts TestStruct
140+
err := json.Unmarshal([]byte(tt.input), &ts)
141+
if err != nil {
142+
t.Fatalf("Unmarshal failed: %v", err)
143+
}
144+
if !slices.Equal([]string(ts.Field), tt.expected) {
145+
t.Errorf("Expected %v, got %v", tt.expected, ts.Field)
146+
}
147+
})
148+
}
149+
}

infra/conf/observatory.go

Lines changed: 5 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -6,14 +6,14 @@ import (
66
"github.com/xtls/xray-core/app/observatory"
77
"github.com/xtls/xray-core/app/observatory/burst"
88
"github.com/xtls/xray-core/common/errors"
9-
"github.com/xtls/xray-core/infra/conf/cfgcommon/duration"
9+
"github.com/xtls/xray-core/infra/conf/cfgcommon/types"
1010
)
1111

1212
type ObservatoryConfig struct {
13-
SubjectSelector []string `json:"subjectSelector"`
14-
ProbeURL string `json:"probeURL"`
15-
ProbeInterval duration.Duration `json:"probeInterval"`
16-
EnableConcurrency bool `json:"enableConcurrency"`
13+
SubjectSelector []string `json:"subjectSelector"`
14+
ProbeURL string `json:"probeURL"`
15+
ProbeInterval types.Duration `json:"probeInterval"`
16+
EnableConcurrency bool `json:"enableConcurrency"`
1717
}
1818

1919
func (o *ObservatoryConfig) Build() (proto.Message, error) {

infra/conf/router_strategy.go

Lines changed: 11 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -1,12 +1,13 @@
11
package conf
22

33
import (
4-
"google.golang.org/protobuf/proto"
54
"strings"
65

6+
"google.golang.org/protobuf/proto"
7+
78
"github.com/xtls/xray-core/app/observatory/burst"
89
"github.com/xtls/xray-core/app/router"
9-
"github.com/xtls/xray-core/infra/conf/cfgcommon/duration"
10+
"github.com/xtls/xray-core/infra/conf/cfgcommon/types"
1011
)
1112

1213
const (
@@ -36,23 +37,23 @@ type strategyLeastLoadConfig struct {
3637
// weight settings
3738
Costs []*router.StrategyWeight `json:"costs,omitempty"`
3839
// ping rtt baselines
39-
Baselines []duration.Duration `json:"baselines,omitempty"`
40+
Baselines []types.Duration `json:"baselines,omitempty"`
4041
// expected nodes count to select
4142
Expected int32 `json:"expected,omitempty"`
4243
// max acceptable rtt, filter away high delay nodes. default 0
43-
MaxRTT duration.Duration `json:"maxRTT,omitempty"`
44+
MaxRTT types.Duration `json:"maxRTT,omitempty"`
4445
// acceptable failure rate
4546
Tolerance float64 `json:"tolerance,omitempty"`
4647
}
4748

4849
// healthCheckSettings holds settings for health Checker
4950
type healthCheckSettings struct {
50-
Destination string `json:"destination"`
51-
Connectivity string `json:"connectivity"`
52-
Interval duration.Duration `json:"interval"`
53-
SamplingCount int `json:"sampling"`
54-
Timeout duration.Duration `json:"timeout"`
55-
HttpMethod string `json:"httpMethod"`
51+
Destination string `json:"destination"`
52+
Connectivity string `json:"connectivity"`
53+
Interval types.Duration `json:"interval"`
54+
SamplingCount int `json:"sampling"`
55+
Timeout types.Duration `json:"timeout"`
56+
HttpMethod string `json:"httpMethod"`
5657
}
5758

5859
func (h healthCheckSettings) Build() (proto.Message, error) {

infra/conf/transport_internet.go

Lines changed: 23 additions & 24 deletions
Original file line numberDiff line numberDiff line change
@@ -15,6 +15,7 @@ import (
1515
"github.com/xtls/xray-core/common/net"
1616
"github.com/xtls/xray-core/common/platform/filesystem"
1717
"github.com/xtls/xray-core/common/serial"
18+
"github.com/xtls/xray-core/infra/conf/cfgcommon/types"
1819
"github.com/xtls/xray-core/transport/internet"
1920
"github.com/xtls/xray-core/transport/internet/finalmask/salamander"
2021
"github.com/xtls/xray-core/transport/internet/httpupgrade"
@@ -568,26 +569,26 @@ func (c *TLSCertConfig) Build() (*tls.Certificate, error) {
568569
}
569570

570571
type TLSConfig struct {
571-
Insecure bool `json:"allowInsecure"`
572-
Certs []*TLSCertConfig `json:"certificates"`
573-
ServerName string `json:"serverName"`
574-
ALPN *StringList `json:"alpn"`
575-
EnableSessionResumption bool `json:"enableSessionResumption"`
576-
DisableSystemRoot bool `json:"disableSystemRoot"`
577-
MinVersion string `json:"minVersion"`
578-
MaxVersion string `json:"maxVersion"`
579-
CipherSuites string `json:"cipherSuites"`
580-
Fingerprint string `json:"fingerprint"`
581-
RejectUnknownSNI bool `json:"rejectUnknownSni"`
582-
PinnedPeerCertSha256 string `json:"pinnedPeerCertSha256"`
583-
CurvePreferences *StringList `json:"curvePreferences"`
584-
MasterKeyLog string `json:"masterKeyLog"`
585-
ServerNameToVerify string `json:"serverNameToVerify"`
586-
VerifyPeerCertInNames []string `json:"verifyPeerCertInNames"`
587-
ECHServerKeys string `json:"echServerKeys"`
588-
ECHConfigList string `json:"echConfigList"`
589-
ECHForceQuery string `json:"echForceQuery"`
590-
ECHSocketSettings *SocketConfig `json:"echSockopt"`
572+
Insecure bool `json:"allowInsecure"`
573+
Certs []*TLSCertConfig `json:"certificates"`
574+
ServerName string `json:"serverName"`
575+
ALPN *StringList `json:"alpn"`
576+
EnableSessionResumption bool `json:"enableSessionResumption"`
577+
DisableSystemRoot bool `json:"disableSystemRoot"`
578+
MinVersion string `json:"minVersion"`
579+
MaxVersion string `json:"maxVersion"`
580+
CipherSuites string `json:"cipherSuites"`
581+
Fingerprint string `json:"fingerprint"`
582+
RejectUnknownSNI bool `json:"rejectUnknownSni"`
583+
PinnedPeerCertSha256 types.ListableSimpleString `json:"pinnedPeerCertSha256"`
584+
CurvePreferences *StringList `json:"curvePreferences"`
585+
MasterKeyLog string `json:"masterKeyLog"`
586+
ServerNameToVerify string `json:"serverNameToVerify"`
587+
VerifyPeerCertInNames types.ListableSimpleString `json:"verifyPeerCertInNames"`
588+
ECHServerKeys string `json:"echServerKeys"`
589+
ECHConfigList string `json:"echConfigList"`
590+
ECHForceQuery string `json:"echForceQuery"`
591+
ECHSocketSettings *SocketConfig `json:"echSockopt"`
591592
}
592593

593594
// Build implements Buildable.
@@ -633,11 +634,9 @@ func (c *TLSConfig) Build() (proto.Message, error) {
633634
}
634635
config.RejectUnknownSni = c.RejectUnknownSNI
635636

636-
if c.PinnedPeerCertSha256 != "" {
637+
if len(c.PinnedPeerCertSha256) != 0 {
637638
config.PinnedPeerCertSha256 = [][]byte{}
638-
// Split by tilde separator
639-
hashes := strings.Split(c.PinnedPeerCertSha256, "~")
640-
for _, v := range hashes {
639+
for _, v := range c.PinnedPeerCertSha256 {
641640
v = strings.TrimSpace(v)
642641
if v == "" {
643642
continue

0 commit comments

Comments
 (0)