diff --git a/commands/all/api.go b/commands/all/api.go deleted file mode 100644 index ad36e76cf..000000000 --- a/commands/all/api.go +++ /dev/null @@ -1,145 +0,0 @@ -package all - -import ( - "context" - "errors" - "fmt" - "strings" - "time" - - "github.com/golang/protobuf/proto" - "google.golang.org/grpc" - - logService "github.com/v2fly/v2ray-core/v4/app/log/command" - statsService "github.com/v2fly/v2ray-core/v4/app/stats/command" - "github.com/v2fly/v2ray-core/v4/commands/base" -) - -// cmdAPI calls an API in an V2Ray process -var cmdAPI = &base.Command{ - UsageLine: "{{.Exec}} api [-server 127.0.0.1:8080] ", - Short: "Call V2Ray API", - Long: ` -Call V2Ray API, API calls in this command have a timeout to the server of 3 seconds. - -The following methods are currently supported: - - LoggerService.RestartLogger - StatsService.GetStats - StatsService.QueryStats - -Examples: - - {{.Exec}} {{.LongName}} --server=127.0.0.1:8080 LoggerService.RestartLogger '' - {{.Exec}} {{.LongName}} --server=127.0.0.1:8080 StatsService.QueryStats 'pattern: "" reset: false' - {{.Exec}} {{.LongName}} --server=127.0.0.1:8080 StatsService.GetStats 'name: "inbound>>>statin>>>traffic>>>downlink" reset: false' - {{.Exec}} {{.LongName}} --server=127.0.0.1:8080 StatsService.GetSysStats '' - `, -} - -func init() { - cmdAPI.Run = executeAPI // break init loop -} - -var ( - apiServerAddrPtr = cmdAPI.Flag.String("server", "127.0.0.1:8080", "") -) - -func executeAPI(cmd *base.Command, args []string) { - unnamedArgs := cmdAPI.Flag.Args() - if len(unnamedArgs) < 2 { - base.Fatalf("service name or request not specified.") - } - - service, method := getServiceMethod(unnamedArgs[0]) - handler, found := serivceHandlerMap[strings.ToLower(service)] - if !found { - base.Fatalf("unknown service: %s", service) - } - - ctx, cancel := context.WithTimeout(context.Background(), 3*time.Second) - defer cancel() - - conn, err := grpc.DialContext(ctx, *apiServerAddrPtr, grpc.WithInsecure(), grpc.WithBlock()) - if err != nil { - base.Fatalf("failed to dial %s", *apiServerAddrPtr) - } - defer conn.Close() - - response, err := handler(ctx, conn, method, unnamedArgs[1]) - if err != nil { - base.Fatalf("failed to call service %s: %s", unnamedArgs[0], err) - } - - fmt.Println(response) -} - -func getServiceMethod(s string) (string, string) { - ss := strings.Split(s, ".") - service := ss[0] - var method string - if len(ss) > 1 { - method = ss[1] - } - return service, method -} - -type serviceHandler func(ctx context.Context, conn *grpc.ClientConn, method string, request string) (string, error) - -var serivceHandlerMap = map[string]serviceHandler{ - "statsservice": callStatsService, - "loggerservice": callLogService, -} - -func callLogService(ctx context.Context, conn *grpc.ClientConn, method string, request string) (string, error) { - client := logService.NewLoggerServiceClient(conn) - - switch strings.ToLower(method) { - case "restartlogger": - r := &logService.RestartLoggerRequest{} - resp, err := client.RestartLogger(ctx, r) - if err != nil { - return "", err - } - return proto.MarshalTextString(resp), nil - default: - return "", errors.New("Unknown method: " + method) - } -} - -func callStatsService(ctx context.Context, conn *grpc.ClientConn, method string, request string) (string, error) { - client := statsService.NewStatsServiceClient(conn) - - switch strings.ToLower(method) { - case "getstats": - r := &statsService.GetStatsRequest{} - if err := proto.UnmarshalText(request, r); err != nil { - return "", err - } - resp, err := client.GetStats(ctx, r) - if err != nil { - return "", err - } - return proto.MarshalTextString(resp), nil - case "querystats": - r := &statsService.QueryStatsRequest{} - if err := proto.UnmarshalText(request, r); err != nil { - return "", err - } - resp, err := client.QueryStats(ctx, r) - if err != nil { - return "", err - } - return proto.MarshalTextString(resp), nil - case "getsysstats": - // SysStatsRequest is an empty message - r := &statsService.SysStatsRequest{} - resp, err := client.GetSysStats(ctx, r) - if err != nil { - return "", err - } - return proto.MarshalTextString(resp), nil - default: - return "", errors.New("Unknown method: " + method) - } -} diff --git a/commands/all/api/api.go b/commands/all/api/api.go new file mode 100644 index 000000000..d76b89e94 --- /dev/null +++ b/commands/all/api/api.go @@ -0,0 +1,23 @@ +package api + +import ( + "v2ray.com/core/commands/base" +) + +// CmdAPI calls an API in an V2Ray process +var CmdAPI = &base.Command{ + UsageLine: "{{.Exec}} api", + Short: "Call V2Ray API", + Long: `{{.Exec}} {{.LongName}} provides tools to manipulate V2Ray via its API. +`, + Commands: []*base.Command{ + cmdRestartLogger, + cmdGetStats, + cmdQueryStats, + cmdSysStats, + cmdAddInbounds, + cmdAddOutbounds, + cmdRemoveInbounds, + cmdRemoveOutbounds, + }, +} diff --git a/commands/all/api/inbounds_add.go b/commands/all/api/inbounds_add.go new file mode 100644 index 000000000..b021c4319 --- /dev/null +++ b/commands/all/api/inbounds_add.go @@ -0,0 +1,78 @@ +package api + +import ( + "fmt" + + handlerService "v2ray.com/core/app/proxyman/command" + "v2ray.com/core/commands/base" + "v2ray.com/core/infra/conf" + "v2ray.com/core/infra/conf/serial" +) + +var cmdAddInbounds = &base.Command{ + CustomFlags: true, + UsageLine: "{{.Exec}} api adi [--server=127.0.0.1:8080] [c2.json]...", + Short: "Add inbounds", + Long: ` +Add inbounds to V2Ray. + +Arguments: + + -s, -server + The API server address. Default 127.0.0.1:8080 + + -t, -timeout + Timeout seconds to call API. Default 3 + +Example: + + {{.Exec}} {{.LongName}} --server=127.0.0.1:8080 c1.json c2.json +`, + Run: executeAddInbounds, +} + +func executeAddInbounds(cmd *base.Command, args []string) { + setSharedFlags(cmd) + cmd.Flag.Parse(args) + unnamedArgs := cmd.Flag.Args() + if len(unnamedArgs) == 0 { + fmt.Println("reading from stdin:") + unnamedArgs = []string{"stdin:"} + } + + ins := make([]conf.InboundDetourConfig, 0) + for _, arg := range unnamedArgs { + r, err := loadArg(arg) + if err != nil { + base.Fatalf("failed to load %s: %s", arg, err) + } + conf, err := serial.DecodeJSONConfig(r) + if err != nil { + base.Fatalf("failed to decode %s: %s", arg, err) + } + ins = append(ins, conf.InboundConfigs...) + } + if len(ins) == 0 { + base.Fatalf("no valid inbound found") + } + + conn, ctx, close := dialAPIServer() + defer close() + + client := handlerService.NewHandlerServiceClient(conn) + for _, in := range ins { + fmt.Println("adding:", in.Tag) + i, err := in.Build() + if err != nil { + base.Fatalf("failed to build conf: %s", err) + } + r := &handlerService.AddInboundRequest{ + Inbound: i, + } + resp, err := client.AddInbound(ctx, r) + if err != nil { + base.Fatalf("failed to add inbound: %s", err) + } + showResponese(resp) + } +} diff --git a/commands/all/api/inbounds_remove.go b/commands/all/api/inbounds_remove.go new file mode 100644 index 000000000..fa9fe8550 --- /dev/null +++ b/commands/all/api/inbounds_remove.go @@ -0,0 +1,79 @@ +package api + +import ( + "fmt" + + handlerService "v2ray.com/core/app/proxyman/command" + "v2ray.com/core/commands/base" + "v2ray.com/core/infra/conf/serial" +) + +var cmdRemoveInbounds = &base.Command{ + CustomFlags: true, + UsageLine: "{{.Exec}} api rmi [--server=127.0.0.1:8080] [json_file] [tag]...", + Short: "Remove inbounds", + Long: ` +Remove inbounds from V2Ray. + +Arguments: + + -s, -server + The API server address. Default 127.0.0.1:8080 + + -t, -timeout + Timeout seconds to call API. Default 3 + +Example: + + {{.Exec}} {{.LongName}} --server=127.0.0.1:8080 c1.json "tag name" +`, + Run: executeRemoveInbounds, +} + +func executeRemoveInbounds(cmd *base.Command, args []string) { + setSharedFlags(cmd) + cmd.Flag.Parse(args) + unnamedArgs := cmd.Flag.Args() + if len(unnamedArgs) == 0 { + fmt.Println("reading from stdin:") + unnamedArgs = []string{"stdin:"} + } + + tags := make([]string, 0) + for _, arg := range unnamedArgs { + if r, err := loadArg(arg); err == nil { + conf, err := serial.DecodeJSONConfig(r) + if err != nil { + base.Fatalf("failed to decode %s: %s", arg, err) + } + ins := conf.InboundConfigs + for _, i := range ins { + tags = append(tags, i.Tag) + } + } else { + // take request as tag + tags = append(tags, arg) + } + } + + if len(tags) == 0 { + base.Fatalf("no inbound to remove") + } + fmt.Println("removing inbounds:", tags) + + conn, ctx, close := dialAPIServer() + defer close() + + client := handlerService.NewHandlerServiceClient(conn) + for _, tag := range tags { + fmt.Println("removing:", tag) + r := &handlerService.RemoveInboundRequest{ + Tag: tag, + } + resp, err := client.RemoveInbound(ctx, r) + if err != nil { + base.Fatalf("failed to remove inbound: %s", err) + } + showResponese(resp) + } +} diff --git a/commands/all/api/logger_restart.go b/commands/all/api/logger_restart.go new file mode 100644 index 000000000..e6de4f74a --- /dev/null +++ b/commands/all/api/logger_restart.go @@ -0,0 +1,40 @@ +package api + +import ( + logService "v2ray.com/core/app/log/command" + "v2ray.com/core/commands/base" +) + +var cmdRestartLogger = &base.Command{ + CustomFlags: true, + UsageLine: "{{.Exec}} api restartlogger [--server=127.0.0.1:8080]", + Short: "Restart the logger", + Long: ` +Restart the logger of V2Ray. + +Arguments: + + -s, -server + The API server address. Default 127.0.0.1:8080 + + -t, -timeout + Timeout seconds to call API. Default 3 +`, + Run: executeRestartLogger, +} + +func executeRestartLogger(cmd *base.Command, args []string) { + setSharedFlags(cmd) + cmd.Flag.Parse(args) + + conn, ctx, close := dialAPIServer() + defer close() + + client := logService.NewLoggerServiceClient(conn) + r := &logService.RestartLoggerRequest{} + resp, err := client.RestartLogger(ctx, r) + if err != nil { + base.Fatalf("failed to restart logger: %s", err) + } + showResponese(resp) +} diff --git a/commands/all/api/outbounds_add.go b/commands/all/api/outbounds_add.go new file mode 100644 index 000000000..a31fa620c --- /dev/null +++ b/commands/all/api/outbounds_add.go @@ -0,0 +1,78 @@ +package api + +import ( + "fmt" + + handlerService "v2ray.com/core/app/proxyman/command" + "v2ray.com/core/commands/base" + "v2ray.com/core/infra/conf" + "v2ray.com/core/infra/conf/serial" +) + +var cmdAddOutbounds = &base.Command{ + CustomFlags: true, + UsageLine: "{{.Exec}} api ado [--server=127.0.0.1:8080] [c2.json]...", + Short: "Add outbounds", + Long: ` +Add outbounds to V2Ray. + +Arguments: + + -s, -server + The API server address. Default 127.0.0.1:8080 + + -t, -timeout + Timeout seconds to call API. Default 3 + +Example: + + {{.Exec}} {{.LongName}} --server=127.0.0.1:8080 c1.json c2.json +`, + Run: executeAddOutbounds, +} + +func executeAddOutbounds(cmd *base.Command, args []string) { + setSharedFlags(cmd) + cmd.Flag.Parse(args) + unnamedArgs := cmd.Flag.Args() + if len(unnamedArgs) == 0 { + fmt.Println("Reading from STDIN") + unnamedArgs = []string{"stdin:"} + } + + outs := make([]conf.OutboundDetourConfig, 0) + for _, arg := range unnamedArgs { + r, err := loadArg(arg) + if err != nil { + base.Fatalf("failed to load %s: %s", arg, err) + } + conf, err := serial.DecodeJSONConfig(r) + if err != nil { + base.Fatalf("failed to decode %s: %s", arg, err) + } + outs = append(outs, conf.OutboundConfigs...) + } + if len(outs) == 0 { + base.Fatalf("no valid outbound found") + } + + conn, ctx, close := dialAPIServer() + defer close() + + client := handlerService.NewHandlerServiceClient(conn) + for _, out := range outs { + fmt.Println("adding:", out.Tag) + o, err := out.Build() + if err != nil { + base.Fatalf("failed to build conf: %s", err) + } + r := &handlerService.AddOutboundRequest{ + Outbound: o, + } + resp, err := client.AddOutbound(ctx, r) + if err != nil { + base.Fatalf("failed to add outbound: %s", err) + } + showResponese(resp) + } +} diff --git a/commands/all/api/outbounds_remove.go b/commands/all/api/outbounds_remove.go new file mode 100644 index 000000000..967ec57e8 --- /dev/null +++ b/commands/all/api/outbounds_remove.go @@ -0,0 +1,78 @@ +package api + +import ( + "fmt" + + handlerService "v2ray.com/core/app/proxyman/command" + "v2ray.com/core/commands/base" + "v2ray.com/core/infra/conf/serial" +) + +var cmdRemoveOutbounds = &base.Command{ + CustomFlags: true, + UsageLine: "{{.Exec}} api rmo [--server=127.0.0.1:8080] [json_file] [tag]...", + Short: "Remove outbounds", + Long: ` +Remove outbounds from V2Ray. + +Arguments: + + -s, -server + The API server address. Default 127.0.0.1:8080 + + -t, -timeout + Timeout seconds to call API. Default 3 + +Example: + + {{.Exec}} {{.LongName}} --server=127.0.0.1:8080 c1.json "tag name" +`, + Run: executeRemoveOutbounds, +} + +func executeRemoveOutbounds(cmd *base.Command, args []string) { + setSharedFlags(cmd) + cmd.Flag.Parse(args) + unnamedArgs := cmd.Flag.Args() + if len(unnamedArgs) == 0 { + fmt.Println("reading from stdin:") + unnamedArgs = []string{"stdin:"} + } + + tags := make([]string, 0) + for _, arg := range unnamedArgs { + if r, err := loadArg(arg); err == nil { + conf, err := serial.DecodeJSONConfig(r) + if err != nil { + base.Fatalf("failed to decode %s: %s", arg, err) + } + outs := conf.OutboundConfigs + for _, o := range outs { + tags = append(tags, o.Tag) + } + } else { + // take request as tag + tags = append(tags, arg) + } + } + + if len(tags) == 0 { + base.Fatalf("no outbound to remove") + } + + conn, ctx, close := dialAPIServer() + defer close() + + client := handlerService.NewHandlerServiceClient(conn) + for _, tag := range tags { + fmt.Println("removing:", tag) + r := &handlerService.RemoveOutboundRequest{ + Tag: tag, + } + resp, err := client.RemoveOutbound(ctx, r) + if err != nil { + base.Fatalf("failed to remove outbound: %s", err) + } + showResponese(resp) + } +} diff --git a/commands/all/api/shared.go b/commands/all/api/shared.go new file mode 100644 index 000000000..830034046 --- /dev/null +++ b/commands/all/api/shared.go @@ -0,0 +1,118 @@ +package api + +import ( + "bytes" + "context" + "fmt" + "io" + "io/ioutil" + "net/http" + "net/url" + "os" + "strings" + "time" + + "google.golang.org/grpc" + "google.golang.org/protobuf/proto" + "v2ray.com/core/commands/base" + "v2ray.com/core/common/buf" +) + +type serviceHandler func(ctx context.Context, conn *grpc.ClientConn, cmd *base.Command, args []string) string + +var ( + apiServerAddrPtr string + apiTimeout int +) + +func setSharedFlags(cmd *base.Command) { + cmd.Flag.StringVar(&apiServerAddrPtr, "s", "127.0.0.1:8080", "") + cmd.Flag.StringVar(&apiServerAddrPtr, "server", "127.0.0.1:8080", "") + cmd.Flag.IntVar(&apiTimeout, "t", 3, "") + cmd.Flag.IntVar(&apiTimeout, "timeout", 3, "") +} + +func dialAPIServer() (conn *grpc.ClientConn, ctx context.Context, close func()) { + ctx, cancel := context.WithTimeout(context.Background(), time.Duration(apiTimeout)*time.Second) + conn, err := grpc.DialContext(ctx, apiServerAddrPtr, grpc.WithInsecure(), grpc.WithBlock()) + if err != nil { + base.Fatalf("failed to dial %s", apiServerAddrPtr) + } + close = func() { + cancel() + conn.Close() + } + return +} + +// loadArg loads one arg, maybe an remote url, or local file path +func loadArg(arg string) (out io.Reader, err error) { + var data []byte + switch { + case strings.HasPrefix(arg, "http://"), strings.HasPrefix(arg, "https://"): + data, err = fetchHTTPContent(arg) + + case arg == "stdin:": + data, err = ioutil.ReadAll(os.Stdin) + + default: + data, err = ioutil.ReadFile(arg) + } + + if err != nil { + return + } + out = bytes.NewBuffer(data) + return +} + +// fetchHTTPContent dials https for remote content +func fetchHTTPContent(target string) ([]byte, error) { + parsedTarget, err := url.Parse(target) + if err != nil { + return nil, err + } + + if s := strings.ToLower(parsedTarget.Scheme); s != "http" && s != "https" { + return nil, fmt.Errorf("invalid scheme: %s", parsedTarget.Scheme) + } + + client := &http.Client{ + Timeout: 30 * time.Second, + } + resp, err := client.Do(&http.Request{ + Method: "GET", + URL: parsedTarget, + Close: true, + }) + if err != nil { + return nil, fmt.Errorf("failed to dial to %s", target) + } + defer resp.Body.Close() + + if resp.StatusCode != 200 { + return nil, fmt.Errorf("unexpected HTTP status code: %d", resp.StatusCode) + } + + content, err := buf.ReadAllToBytes(resp.Body) + if err != nil { + return nil, fmt.Errorf("failed to read HTTP response") + } + + return content, nil +} + +func showResponese(m proto.Message) { + msg := "" + bs, err := proto.Marshal(m) + if err != nil { + msg = err.Error() + } else { + msg = string(bs) + msg = strings.TrimSpace(msg) + } + if msg == "" { + return + } + fmt.Println(msg) +} diff --git a/commands/all/api/stats_get.go b/commands/all/api/stats_get.go new file mode 100644 index 000000000..7e9b69ed0 --- /dev/null +++ b/commands/all/api/stats_get.go @@ -0,0 +1,55 @@ +package api + +import ( + statsService "v2ray.com/core/app/stats/command" + "v2ray.com/core/commands/base" +) + +var cmdGetStats = &base.Command{ + CustomFlags: true, + UsageLine: "{{.Exec}} api stats [--server=127.0.0.1:8080] [-name '']", + Short: "Get statistics", + Long: ` +Get statistics from V2Ray. + +Arguments: + + -s, -server + The API server address. Default 127.0.0.1:8080 + + -t, -timeout + Timeout seconds to call API. Default 3 + + -name + Name of the stat counter. + + -reset + Reset the counter to fetching its value. + +Example: + + {{.Exec}} {{.LongName}} --server=127.0.0.1:8080 -name "inbound>>>statin>>>traffic>>>downlink" +`, + Run: executeGetStats, +} + +func executeGetStats(cmd *base.Command, args []string) { + setSharedFlags(cmd) + statName := cmd.Flag.String("name", "", "") + reset := cmd.Flag.Bool("reset", false, "") + cmd.Flag.Parse(args) + + conn, ctx, close := dialAPIServer() + defer close() + + client := statsService.NewStatsServiceClient(conn) + r := &statsService.GetStatsRequest{ + Name: *statName, + Reset_: *reset, + } + resp, err := client.GetStats(ctx, r) + if err != nil { + base.Fatalf("failed to get stats: %s", err) + } + showResponese(resp) +} diff --git a/commands/all/api/stats_query.go b/commands/all/api/stats_query.go new file mode 100644 index 000000000..65670d399 --- /dev/null +++ b/commands/all/api/stats_query.go @@ -0,0 +1,55 @@ +package api + +import ( + statsService "v2ray.com/core/app/stats/command" + "v2ray.com/core/commands/base" +) + +var cmdQueryStats = &base.Command{ + CustomFlags: true, + UsageLine: "{{.Exec}} api statsquery [--server=127.0.0.1:8080] [-pattern '']", + Short: "Query statistics", + Long: ` +Query statistics from V2Ray. + +Arguments: + + -s, -server + The API server address. Default 127.0.0.1:8080 + + -t, -timeout + Timeout seconds to call API. Default 3 + + -pattern + Pattern of the query. + + -reset + Reset the counter to fetching its value. + +Example: + + {{.Exec}} {{.LongName}} --server=127.0.0.1:8080 -pattern "counter_" +`, + Run: executeQueryStats, +} + +func executeQueryStats(cmd *base.Command, args []string) { + setSharedFlags(cmd) + pattern := cmd.Flag.String("pattern", "", "") + reset := cmd.Flag.Bool("reset", false, "") + cmd.Flag.Parse(args) + + conn, ctx, close := dialAPIServer() + defer close() + + client := statsService.NewStatsServiceClient(conn) + r := &statsService.QueryStatsRequest{ + Pattern: *pattern, + Reset_: *reset, + } + resp, err := client.QueryStats(ctx, r) + if err != nil { + base.Fatalf("failed to query stats: %s", err) + } + showResponese(resp) +} diff --git a/commands/all/api/stats_sys.go b/commands/all/api/stats_sys.go new file mode 100644 index 000000000..257372d39 --- /dev/null +++ b/commands/all/api/stats_sys.go @@ -0,0 +1,40 @@ +package api + +import ( + statsService "v2ray.com/core/app/stats/command" + "v2ray.com/core/commands/base" +) + +var cmdSysStats = &base.Command{ + CustomFlags: true, + UsageLine: "{{.Exec}} api statssys [--server=127.0.0.1:8080]", + Short: "Get system statistics", + Long: ` +Get system statistics from V2Ray. + +Arguments: + + -s, -server + The API server address. Default 127.0.0.1:8080 + + -t, -timeout + Timeout seconds to call API. Default 3 +`, + Run: executeSysStats, +} + +func executeSysStats(cmd *base.Command, args []string) { + setSharedFlags(cmd) + cmd.Flag.Parse(args) + + conn, ctx, close := dialAPIServer() + defer close() + + client := statsService.NewStatsServiceClient(conn) + r := &statsService.SysStatsRequest{} + resp, err := client.GetSysStats(ctx, r) + if err != nil { + base.Fatalf("failed to get sys stats: %s", err) + } + showResponese(resp) +} diff --git a/commands/all/commands.go b/commands/all/commands.go index 3842e0332..1b533b79a 100644 --- a/commands/all/commands.go +++ b/commands/all/commands.go @@ -1,16 +1,20 @@ package all -import "github.com/v2fly/v2ray-core/v4/commands/base" +import ( + "github.com/v2fly/v2ray-core/v4/commands/all/api" + "github.com/v2fly/v2ray-core/v4/commands/all/tls" + "github.com/v2fly/v2ray-core/v4/commands/base" +) // go:generate go run v2ray.com/core/common/errors/errorgen func init() { base.RootCommand.Commands = append( base.RootCommand.Commands, - cmdAPI, + api.CmdAPI, cmdConvert, cmdLove, - cmdTLS, + tls.CmdTLS, cmdUUID, cmdVerify, cmdMerge, diff --git a/commands/all/tls.go b/commands/all/tls.go deleted file mode 100644 index 627efec88..000000000 --- a/commands/all/tls.go +++ /dev/null @@ -1,18 +0,0 @@ -package all - -import ( - "github.com/v2fly/v2ray-core/v4/commands/all/tlscmd" - "github.com/v2fly/v2ray-core/v4/commands/base" -) - -var cmdTLS = &base.Command{ - UsageLine: "{{.Exec}} tls", - Short: "TLS tools", - Long: `{{.Exec}} tls provides tools for TLS. - `, - - Commands: []*base.Command{ - tlscmd.CmdCert, - tlscmd.CmdPing, - }, -} diff --git a/commands/all/tlscmd/cert.go b/commands/all/tls/cert.go similarity index 84% rename from commands/all/tlscmd/cert.go rename to commands/all/tls/cert.go index 53d5e01b0..ae19a07c8 100644 --- a/commands/all/tlscmd/cert.go +++ b/commands/all/tls/cert.go @@ -1,4 +1,4 @@ -package tlscmd +package tls import ( "context" @@ -14,8 +14,8 @@ import ( "github.com/v2fly/v2ray-core/v4/common/task" ) -// CmdCert is the tls cert command -var CmdCert = &base.Command{ +// cmdCert is the tls cert command +var cmdCert = &base.Command{ UsageLine: "{{.Exec}} tls cert [--ca] [--domain=v2ray.com] [--expire=240h]", Short: "Generate TLS certificates", Long: ` @@ -44,22 +44,22 @@ Arguments: } func init() { - CmdCert.Run = executeCert // break init loop + cmdCert.Run = executeCert // break init loop } var ( certDomainNames stringList _ = func() bool { - CmdCert.Flag.Var(&certDomainNames, "domain", "Domain name for the certificate") + cmdCert.Flag.Var(&certDomainNames, "domain", "Domain name for the certificate") return true }() - certCommonName = CmdCert.Flag.String("name", "V2Ray Inc", "The common name of this certificate") - certOrganization = CmdCert.Flag.String("org", "V2Ray Inc", "Organization of the certificate") - certIsCA = CmdCert.Flag.Bool("ca", false, "Whether this certificate is a CA") - certJSONOutput = CmdCert.Flag.Bool("json", true, "Print certificate in JSON format") - certFileOutput = CmdCert.Flag.String("file", "", "Save certificate in file.") - certExpire = CmdCert.Flag.Duration("expire", time.Hour*24*90 /* 90 days */, "Time until the certificate expires. Default value 3 months.") + certCommonName = cmdCert.Flag.String("name", "V2Ray Inc", "The common name of this certificate") + certOrganization = cmdCert.Flag.String("org", "V2Ray Inc", "Organization of the certificate") + certIsCA = cmdCert.Flag.Bool("ca", false, "Whether this certificate is a CA") + certJSONOutput = cmdCert.Flag.Bool("json", true, "Print certificate in JSON format") + certFileOutput = cmdCert.Flag.String("file", "", "Save certificate in file.") + certExpire = cmdCert.Flag.Duration("expire", time.Hour*24*90 /* 90 days */, "Time until the certificate expires. Default value 3 months.") ) func executeCert(cmd *base.Command, args []string) { diff --git a/commands/all/tlscmd/ping.go b/commands/all/tls/ping.go similarity index 92% rename from commands/all/tlscmd/ping.go rename to commands/all/tls/ping.go index cc0860226..ac190cf4d 100644 --- a/commands/all/tlscmd/ping.go +++ b/commands/all/tls/ping.go @@ -1,4 +1,4 @@ -package tlscmd +package tls import ( "crypto/tls" @@ -11,8 +11,8 @@ import ( v2tls "github.com/v2fly/v2ray-core/v4/transport/internet/tls" ) -// CmdPing is the tls ping command -var CmdPing = &base.Command{ +// cmdPing is the tls ping command +var cmdPing = &base.Command{ UsageLine: "{{.Exec}} tls ping [-ip ] ", Short: "Ping the domain with TLS handshake", Long: ` @@ -26,19 +26,19 @@ Arguments: } func init() { - CmdPing.Run = executePing // break init loop + cmdPing.Run = executePing // break init loop } var ( - pingIPStr = CmdPing.Flag.String("ip", "", "") + pingIPStr = cmdPing.Flag.String("ip", "", "") ) func executePing(cmd *base.Command, args []string) { - if CmdPing.Flag.NArg() < 1 { + if cmdPing.Flag.NArg() < 1 { base.Fatalf("domain not specified") } - domain := CmdPing.Flag.Arg(0) + domain := cmdPing.Flag.Arg(0) fmt.Println("Tls ping: ", domain) var ip net.IP diff --git a/commands/all/tls/tls.go b/commands/all/tls/tls.go new file mode 100644 index 000000000..a9dec0c34 --- /dev/null +++ b/commands/all/tls/tls.go @@ -0,0 +1,18 @@ +package tls + +import ( + "v2ray.com/core/commands/base" +) + +// CmdTLS holds all tls sub commands +var CmdTLS = &base.Command{ + UsageLine: "{{.Exec}} tls", + Short: "TLS tools", + Long: `{{.Exec}} {{.LongName}} provides tools for TLS. + `, + + Commands: []*base.Command{ + cmdCert, + cmdPing, + }, +} diff --git a/commands/base/command.go b/commands/base/command.go index 6fa606fda..123453d50 100644 --- a/commands/base/command.go +++ b/commands/base/command.go @@ -22,13 +22,19 @@ type Command struct { Run func(cmd *Command, args []string) // UsageLine is the one-line usage message. - // The words between "go" and the first flag or argument in the line are taken to be the command name. + // The words between the first word (the "executable name") and the first flag or argument in the line are taken to be the command name. + // + // UsageLine supports go template syntax. It's recommended to use "{{.Exec}}" instead of hardcoding name UsageLine string // Short is the short description shown in the 'go help' output. + // + // Note: Short does not support go template syntax. Short string // Long is the long message shown in the 'go help ' output. + // + // Long supports go template syntax. It's recommended to use "{{.Exec}}", "{{.LongName}}" instead of hardcoding strings Long string // Flag is a set of flags specific to this command. @@ -44,16 +50,18 @@ type Command struct { Commands []*Command } -// LongName returns the command's long name: all the words in the usage line between "go" and a flag or argument, +// LongName returns the command's long name: all the words in the usage line between first word (e.g. "v2ray") and a flag or argument, func (c *Command) LongName() string { name := c.UsageLine if i := strings.Index(name, " ["); i >= 0 { - name = name[:i] + name = strings.TrimSpace(name[:i]) } - if name == CommandEnv.Exec { - return "" + if i := strings.Index(name, " "); i >= 0 { + name = name[i+1:] + } else { + name = "" } - return strings.TrimPrefix(name, CommandEnv.Exec+" ") + return strings.TrimSpace(name) } // Name returns the command's short name: the last word in the usage line before a flag or argument. @@ -62,11 +70,12 @@ func (c *Command) Name() string { if i := strings.LastIndex(name, " "); i >= 0 { name = name[i+1:] } - return name + return strings.TrimSpace(name) } // Usage prints usage of the Command func (c *Command) Usage() { + buildCommandText(c) fmt.Fprintf(os.Stderr, "usage: %s\n", c.UsageLine) fmt.Fprintf(os.Stderr, "Run '%s help %s' for details.\n", CommandEnv.Exec, c.LongName()) SetExitStatus(2) diff --git a/commands/base/execute.go b/commands/base/execute.go index da73a4829..802f01c27 100644 --- a/commands/base/execute.go +++ b/commands/base/execute.go @@ -14,7 +14,6 @@ import ( // Execute excute the commands func Execute() { - buildCommandsText(RootCommand) flag.Parse() args := flag.Args() if len(args) < 1 { @@ -61,6 +60,7 @@ BigCmdLoop: args = cmd.Flag.Args() } + buildCommandText(cmd) cmd.Run(cmd, args) Exit() return diff --git a/commands/base/help.go b/commands/base/help.go index 540c5ca89..eefc91f12 100644 --- a/commands/base/help.go +++ b/commands/base/help.go @@ -41,6 +41,7 @@ Args: if len(cmd.Commands) > 0 { PrintUsage(os.Stdout, cmd) } else { + buildCommandText(cmd) tmpl(os.Stdout, helpTemplate, makeTmplData(cmd)) } } @@ -119,24 +120,21 @@ func width(width int, value string) string { // PrintUsage prints usage of cmd to w func PrintUsage(w io.Writer, cmd *Command) { + buildCommandText(cmd) bw := bufio.NewWriter(w) tmpl(bw, usageTemplate, makeTmplData(cmd)) bw.Flush() } -// buildCommandsText build text of command and its children as template -func buildCommandsText(cmd *Command) { - buildCommandText(cmd) - for _, cmd := range cmd.Commands { - buildCommandsText(cmd) - } -} - // buildCommandText build command text as template func buildCommandText(cmd *Command) { - cmd.UsageLine = buildText(cmd.UsageLine, makeTmplData(cmd)) - cmd.Short = buildText(cmd.Short, makeTmplData(cmd)) - cmd.Long = buildText(cmd.Long, makeTmplData(cmd)) + data := makeTmplData(cmd) + cmd.UsageLine = buildText(cmd.UsageLine, data) + // DO NOT SUPPORT ".Short": + // - It's not necessary + // - Or, we have to build text for all sub commands of current command, like "v2ray help api" + // cmd.Short = buildText(cmd.Short, data) + cmd.Long = buildText(cmd.Long, data) } func buildText(text string, data interface{}) string {