mirror of
https://github.com/XTLS/Xray-core.git
synced 2024-12-27 16:09:47 +00:00
Add sub-command "-dump" to "run". (#2854)
* Add MarshalToJson(). * Add cmd arg -dump for printing out merged multiple json configs. --------- Co-authored-by: nobody <nobody@nowhere.mars>
This commit is contained in:
parent
006cf491e5
commit
44bb83033f
@ -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:
|
||||
|
173
common/reflect/marshal.go
Normal file
173
common/reflect/marshal.go
Normal file
@ -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
|
||||
}
|
187
common/reflect/marshal_test.go
Normal file
187
common/reflect/marshal_test.go
Normal file
@ -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"
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
]
|
||||
}`
|
||||
}
|
@ -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":
|
||||
|
@ -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
|
||||
}
|
||||
|
49
main/run.go
49
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)
|
||||
|
||||
|
Loading…
Reference in New Issue
Block a user