diff --git a/common/log/logger.go b/common/log/logger.go index 79507964..d964a212 100644 --- a/common/log/logger.go +++ b/common/log/logger.go @@ -27,6 +27,11 @@ type generalLogger struct { done *done.Instance } +type serverityLogger struct { + inner *generalLogger + logLevel Severity +} + // NewLogger returns a generic log handler that can handle all type of messages. func NewLogger(logWriterCreator WriterCreator) Handler { return &generalLogger{ @@ -37,6 +42,32 @@ func NewLogger(logWriterCreator WriterCreator) Handler { } } +func ReplaceWithSeverityLogger(serverity Severity) { + w := CreateStdoutLogWriter() + g := &generalLogger{ + creator: w, + buffer: make(chan Message, 16), + access: semaphore.New(1), + done: done.New(), + } + s := &serverityLogger{ + inner: g, + logLevel: serverity, + } + RegisterHandler(s) +} + +func (l *serverityLogger) Handle(msg Message) { + switch msg := msg.(type) { + case *GeneralMessage: + if msg.Severity <= l.logLevel { + l.inner.Handle(msg) + } + default: + l.inner.Handle(msg) + } +} + func (l *generalLogger) run() { defer l.access.Signal() @@ -67,6 +98,7 @@ func (l *generalLogger) run() { } func (l *generalLogger) Handle(msg Message) { + select { case l.buffer <- msg: default: diff --git a/common/reflect/marshal.go b/common/reflect/marshal.go new file mode 100644 index 00000000..96e83351 --- /dev/null +++ b/common/reflect/marshal.go @@ -0,0 +1,173 @@ +package reflect + +import ( + "encoding/json" + "reflect" + "slices" + + cserial "github.com/xtls/xray-core/common/serial" +) + +func MarshalToJson(v interface{}) (string, bool) { + if itf := marshalInterface(v, true); itf != nil { + if b, err := json.MarshalIndent(itf, "", " "); err == nil { + return string(b[:]), true + } + } + return "", false +} + +func marshalTypedMessage(v *cserial.TypedMessage, ignoreNullValue bool) interface{} { + tmsg, err := v.GetInstance() + if err != nil { + return nil + } + r := marshalInterface(tmsg, ignoreNullValue) + if msg, ok := r.(map[string]interface{}); ok { + msg["_TypedMessage_"] = v.Type + } + return r +} + +func marshalSlice(v reflect.Value, ignoreNullValue bool) interface{} { + r := make([]interface{}, 0) + for i := 0; i < v.Len(); i++ { + rv := v.Index(i) + if rv.CanInterface() { + value := rv.Interface() + r = append(r, marshalInterface(value, ignoreNullValue)) + } + } + return r +} + +func marshalStruct(v reflect.Value, ignoreNullValue bool) interface{} { + r := make(map[string]interface{}) + t := v.Type() + for i := 0; i < v.NumField(); i++ { + rv := v.Field(i) + if rv.CanInterface() { + ft := t.Field(i) + name := ft.Name + value := rv.Interface() + tv := marshalInterface(value, ignoreNullValue) + if tv != nil || !ignoreNullValue { + r[name] = tv + } + } + } + return r +} + +func marshalMap(v reflect.Value, ignoreNullValue bool) interface{} { + // policy.level is map[uint32] *struct + kt := v.Type().Key() + vt := reflect.TypeOf((*interface{})(nil)) + mt := reflect.MapOf(kt, vt) + r := reflect.MakeMap(mt) + for _, key := range v.MapKeys() { + rv := v.MapIndex(key) + if rv.CanInterface() { + iv := rv.Interface() + tv := marshalInterface(iv, ignoreNullValue) + if tv != nil || !ignoreNullValue { + r.SetMapIndex(key, reflect.ValueOf(&tv)) + } + } + } + return r.Interface() +} + +func marshalIString(v interface{}) (r string, ok bool) { + defer func() { + if err := recover(); err != nil { + r = "" + ok = false + } + }() + + if iStringFn, ok := v.(interface{ String() string }); ok { + return iStringFn.String(), true + } + return "", false +} + +func marshalKnownType(v interface{}, ignoreNullValue bool) (interface{}, bool) { + switch ty := v.(type) { + case cserial.TypedMessage: + return marshalTypedMessage(&ty, ignoreNullValue), true + case *cserial.TypedMessage: + return marshalTypedMessage(ty, ignoreNullValue), true + case map[string]json.RawMessage: + return ty, true + case []json.RawMessage: + return ty, true + case *json.RawMessage: + return ty, true + case json.RawMessage: + return ty, true + default: + return nil, false + } +} + +var valueKinds = []reflect.Kind{ + reflect.Bool, + reflect.Int, + reflect.Int8, + reflect.Int16, + reflect.Int32, + reflect.Int64, + reflect.Uint, + reflect.Uint8, + reflect.Uint16, + reflect.Uint32, + reflect.Uint64, + reflect.Uintptr, + reflect.Float32, + reflect.Float64, + reflect.Complex64, + reflect.Complex128, + reflect.String, +} + +func isValueKind(kind reflect.Kind) bool { + return slices.Contains(valueKinds, kind) +} + +func marshalInterface(v interface{}, ignoreNullValue bool) interface{} { + + if r, ok := marshalKnownType(v, ignoreNullValue); ok { + return r + } + + rv := reflect.ValueOf(v) + if rv.Kind() == reflect.Ptr { + rv = rv.Elem() + } + k := rv.Kind() + if k == reflect.Invalid { + return nil + } + if isValueKind(k) { + return v + } + + switch k { + case reflect.Struct: + return marshalStruct(rv, ignoreNullValue) + case reflect.Slice: + return marshalSlice(rv, ignoreNullValue) + case reflect.Array: + return marshalSlice(rv, ignoreNullValue) + case reflect.Map: + return marshalMap(rv, ignoreNullValue) + default: + break + } + + if str, ok := marshalIString(v); ok { + return str + } + return nil +} diff --git a/common/reflect/marshal_test.go b/common/reflect/marshal_test.go new file mode 100644 index 00000000..377ad4e9 --- /dev/null +++ b/common/reflect/marshal_test.go @@ -0,0 +1,187 @@ +package reflect_test + +import ( + "bytes" + "encoding/json" + "strings" + "testing" + + . "github.com/xtls/xray-core/common/reflect" + cserial "github.com/xtls/xray-core/common/serial" + iserial "github.com/xtls/xray-core/infra/conf/serial" +) + +func TestMashalStruct(t *testing.T) { + type Foo = struct { + N int `json:"n"` + Np *int `json:"np"` + S string `json:"s"` + Arr *[]map[string]map[string]string `json:"arr"` + } + + n := 1 + np := &n + arr := make([]map[string]map[string]string, 0) + m1 := make(map[string]map[string]string, 0) + m2 := make(map[string]string, 0) + m2["hello"] = "world" + m1["foo"] = m2 + + arr = append(arr, m1) + + f1 := Foo{ + N: n, + Np: np, + S: "hello", + Arr: &arr, + } + + s, ok1 := MarshalToJson(f1) + sp, ok2 := MarshalToJson(&f1) + + if !ok1 || !ok2 || s != sp { + t.Error("marshal failed") + } + + f2 := Foo{} + if json.Unmarshal([]byte(s), &f2) != nil { + t.Error("json unmarshal failed") + } + + v := (*f2.Arr)[0]["foo"]["hello"] + + if f1.N != f2.N || *(f1.Np) != *(f2.Np) || f1.S != f2.S || v != "world" { + t.Error("f1 not equal to f2") + } +} + +func TestMarshalConfigJson(t *testing.T) { + + buf := bytes.NewBufferString(getConfig()) + config, err := iserial.DecodeJSONConfig(buf) + if err != nil { + t.Error("decode JSON config failed") + } + + bc, err := config.Build() + if err != nil { + t.Error("build core config failed") + } + + tmsg := cserial.ToTypedMessage(bc) + tc, ok := MarshalToJson(tmsg) + if !ok { + t.Error("marshal config failed") + } + + // t.Log(tc) + + keywords := []string{ + "4784f9b8-a879-4fec-9718-ebddefa47750", + "bing.com", + "DomainStrategy", + "InboundTag", + "Level", + "Stats", + "UserDownlink", + "UserUplink", + "System", + "InboundDownlink", + "OutboundUplink", + } + for _, kw := range keywords { + if !strings.Contains(tc, kw) { + t.Error("marshaled config error") + } + } +} + +func getConfig() string { + return `{ + "log": { + "loglevel": "debug" + }, + "stats": {}, + "policy": { + "levels": { + "0": { + "statsUserUplink": true, + "statsUserDownlink": true + } + }, + "system": { + "statsInboundUplink": true, + "statsInboundDownlink": true, + "statsOutboundUplink": true, + "statsOutboundDownlink": true + } + }, + "inbounds": [ + { + "tag": "agentin", + "protocol": "http", + "port": 8080, + "listen": "127.0.0.1", + "settings": {} + }, + { + "listen": "127.0.0.1", + "port": 10085, + "protocol": "dokodemo-door", + "settings": { + "address": "127.0.0.1" + }, + "tag": "api-in" + } + ], + "api": { + "tag": "api", + "services": [ + "HandlerService", + "StatsService" + ] + }, + "routing": { + "rules": [ + { + "inboundTag": [ + "api-in" + ], + "outboundTag": "api", + "type": "field" + } + ], + "domainStrategy": "AsIs" + }, + "outbounds": [ + { + "protocol": "vless", + "settings": { + "vnext": [ + { + "address": "1.2.3.4", + "port": 1234, + "users": [ + { + "id": "4784f9b8-a879-4fec-9718-ebddefa47750", + "encryption": "none" + } + ] + } + ] + }, + "tag": "agentout", + "streamSettings": { + "network": "ws", + "security": "none", + "wsSettings": { + "path": "/?ed=2048", + "headers": { + "Host": "bing.com" + } + } + } + } + ] + }` +} diff --git a/core/config.go b/core/config.go index f4077449..ec5ad6a4 100644 --- a/core/config.go +++ b/core/config.go @@ -2,6 +2,7 @@ package core import ( "io" + "slices" "strings" "github.com/xtls/xray-core/common" @@ -24,10 +25,14 @@ type ConfigLoader func(input interface{}) (*Config, error) // ConfigBuilder is a builder to build core.Config from filenames and formats type ConfigBuilder func(files []string, formats []string) (*Config, error) +// ConfigMerger merge multiple json configs into on config +type ConfigsMerger func(files []string, formats []string) (string, error) + var ( configLoaderByName = make(map[string]*ConfigFormat) configLoaderByExt = make(map[string]*ConfigFormat) ConfigBuilderForFiles ConfigBuilder + ConfigMergedFormFiles ConfigsMerger ) // RegisterConfigLoader add a new ConfigLoader. @@ -49,6 +54,20 @@ func RegisterConfigLoader(format *ConfigFormat) error { return nil } +func GetMergedConfig(args cmdarg.Arg) (string, error) { + files := make([]string, 0) + formats := make([]string, 0) + supported := []string{"json", "yaml", "toml"} + for _, file := range args { + format := getFormat(file) + if slices.Contains(supported, format) { + files = append(files, file) + formats = append(formats, format) + } + } + return ConfigMergedFormFiles(files, formats) +} + func GetFormatByExtension(ext string) string { switch strings.ToLower(ext) { case "pb", "protobuf": diff --git a/infra/conf/serial/builder.go b/infra/conf/serial/builder.go index 443dbdb0..88ea9e65 100644 --- a/infra/conf/serial/builder.go +++ b/infra/conf/serial/builder.go @@ -3,12 +3,25 @@ package serial import ( "io" + creflect "github.com/xtls/xray-core/common/reflect" "github.com/xtls/xray-core/core" "github.com/xtls/xray-core/infra/conf" "github.com/xtls/xray-core/main/confloader" ) -func BuildConfig(files []string, formats []string) (*core.Config, error) { +func MergeConfigFromFiles(files []string, formats []string) (string, error) { + c, err := mergeConfigs(files, formats) + if err != nil { + return "", err + } + + if j, ok := creflect.MarshalToJson(c); ok { + return j, nil + } + return "", newError("marshal to json failed.").AtError() +} + +func mergeConfigs(files []string, formats []string) (*conf.Config, error) { cf := &conf.Config{} for i, file := range files { newError("Reading config: ", file).AtInfo().WriteToLog() @@ -26,7 +39,15 @@ func BuildConfig(files []string, formats []string) (*core.Config, error) { } cf.Override(c, file) } - return cf.Build() + return cf, nil +} + +func BuildConfig(files []string, formats []string) (*core.Config, error) { + config, err := mergeConfigs(files, formats) + if err != nil { + return nil, err + } + return config.Build() } type readerDecoder func(io.Reader) (*conf.Config, error) @@ -39,4 +60,5 @@ func init() { ReaderDecoderByFormat["toml"] = DecodeTOMLConfig core.ConfigBuilderForFiles = BuildConfig + core.ConfigMergedFormFiles = MergeConfigFromFiles } diff --git a/main/run.go b/main/run.go index 1f8a4b88..f54d7480 100644 --- a/main/run.go +++ b/main/run.go @@ -12,8 +12,10 @@ import ( "runtime/debug" "strings" "syscall" + "time" "github.com/xtls/xray-core/common/cmdarg" + clog "github.com/xtls/xray-core/common/log" "github.com/xtls/xray-core/common/platform" "github.com/xtls/xray-core/core" "github.com/xtls/xray-core/main/commands/base" @@ -34,7 +36,9 @@ The -format=json flag sets the format of config files. Default "auto". The -test flag tells Xray to test config files only, -without launching the server +without launching the server. + +The -dump flag tells Xray to print the merged config. `, } @@ -45,6 +49,7 @@ func init() { var ( configFiles cmdarg.Arg // "Config file for Xray.", the option is customed type, parse in main configDir string + dump = cmdRun.Flag.Bool("dump", false, "Dump merged config only, without launching Xray server.") test = cmdRun.Flag.Bool("test", false, "Test config file only, without launching Xray server.") format = cmdRun.Flag.String("format", "auto", "Format of input file.") @@ -61,6 +66,12 @@ var ( ) func executeRun(cmd *base.Command, args []string) { + if *dump { + clog.ReplaceWithSeverityLogger(clog.Severity_Warning) + errCode := dumpConfig() + os.Exit(errCode) + } + printVersion() server, err := startXray() if err != nil { @@ -97,6 +108,18 @@ func executeRun(cmd *base.Command, args []string) { } } +func dumpConfig() int { + files := getConfigFilePath(false) + if config, err := core.GetMergedConfig(files); err != nil { + fmt.Println(err) + time.Sleep(1 * time.Second) + return 23 + } else { + fmt.Print(config) + } + return 0 +} + func fileExists(file string) bool { info, err := os.Stat(file) return err == nil && !info.IsDir() @@ -139,12 +162,16 @@ func readConfDir(dirPath string) { } } -func getConfigFilePath() cmdarg.Arg { +func getConfigFilePath(verbose bool) cmdarg.Arg { if dirExists(configDir) { - log.Println("Using confdir from arg:", configDir) + if verbose { + log.Println("Using confdir from arg:", configDir) + } readConfDir(configDir) } else if envConfDir := platform.GetConfDirPath(); dirExists(envConfDir) { - log.Println("Using confdir from env:", envConfDir) + if verbose { + log.Println("Using confdir from env:", envConfDir) + } readConfDir(envConfDir) } @@ -155,17 +182,23 @@ func getConfigFilePath() cmdarg.Arg { if workingDir, err := os.Getwd(); err == nil { configFile := filepath.Join(workingDir, "config.json") if fileExists(configFile) { - log.Println("Using default config: ", configFile) + if verbose { + log.Println("Using default config: ", configFile) + } return cmdarg.Arg{configFile} } } if configFile := platform.GetConfigurationPath(); fileExists(configFile) { - log.Println("Using config from env: ", configFile) + if verbose { + log.Println("Using config from env: ", configFile) + } return cmdarg.Arg{configFile} } - log.Println("Using config from STDIN") + if verbose { + log.Println("Using config from STDIN") + } return cmdarg.Arg{"stdin:"} } @@ -178,7 +211,7 @@ func getConfigFormat() string { } func startXray() (core.Server, error) { - configFiles := getConfigFilePath() + configFiles := getConfigFilePath(true) // config, err := core.LoadConfig(getConfigFormat(), configFiles[0], configFiles)