Browse Source

go style commands, merge v2ctl commands, v5 oriented (rebased from 521120d19648bc5ce0494fb915ca933101ea2250)

Jebbs 5 years ago
parent
commit
a96e093432

+ 33 - 46
infra/control/api.go → commands/all/api.go

@@ -1,9 +1,8 @@
-package control
+package all
 
 import (
 	"context"
 	"errors"
-	"flag"
 	"fmt"
 	"strings"
 	"time"
@@ -13,71 +12,66 @@ import (
 
 	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/common"
+	"github.com/v2fly/v2ray-core/v4/commands/base"
 )
 
-type APICommand struct{}
+// cmdAPI calls an API in an V2Ray process
+var cmdAPI = &base.Command{
+	UsageLine: "{{.Exec}} api [-server 127.0.0.1:8080] <action> <parameter>",
+	Short:     "Call V2Ray API",
+	Long: `
+Call V2Ray API, API calls in this command have a timeout to the server of 3 seconds.
 
-func (c *APICommand) Name() string {
-	return "api"
-}
+The following methods are currently supported:
 
-func (c *APICommand) Description() Description {
-	return Description{
-		Short: "Call V2Ray API",
-		Usage: []string{
-			"v2ctl api [--server=127.0.0.1:8080] Service.Method Request",
-			"Call an API in an V2Ray process.",
-			"The following methods are currently supported:",
-			"\tLoggerService.RestartLogger",
-			"\tStatsService.GetStats",
-			"\tStatsService.QueryStats",
-			"API calls in this command have a timeout to the server of 3 seconds.",
-			"Examples:",
-			"v2ctl api --server=127.0.0.1:8080 LoggerService.RestartLogger '' ",
-			"v2ctl api --server=127.0.0.1:8080 StatsService.QueryStats 'pattern: \"\" reset: false'",
-			"v2ctl api --server=127.0.0.1:8080 StatsService.GetStats 'name: \"inbound>>>statin>>>traffic>>>downlink\" reset: false'",
-			"v2ctl api --server=127.0.0.1:8080 StatsService.GetSysStats ''",
-		},
-	}
-}
+	LoggerService.RestartLogger
+	StatsService.GetStats
+	StatsService.QueryStats
 
-func (c *APICommand) Execute(args []string) error {
-	fs := flag.NewFlagSet(c.Name(), flag.ContinueOnError)
+Examples:
 
-	serverAddrPtr := fs.String("server", "127.0.0.1:8080", "Server address")
+	{{.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 ''
+	`,
+}
 
-	if err := fs.Parse(args); err != nil {
-		return err
-	}
+func init() {
+	cmdAPI.Run = executeAPI // break init loop
+}
 
-	unnamedArgs := fs.Args()
+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 {
-		return newError("service name or request not specified.")
+		base.Fatalf("service name or request not specified.")
 	}
 
 	service, method := getServiceMethod(unnamedArgs[0])
 	handler, found := serivceHandlerMap[strings.ToLower(service)]
 	if !found {
-		return newError("unknown service: ", service)
+		base.Fatalf("unknown service: %s", service)
 	}
 
 	ctx, cancel := context.WithTimeout(context.Background(), 3*time.Second)
 	defer cancel()
 
-	conn, err := grpc.DialContext(ctx, *serverAddrPtr, grpc.WithInsecure(), grpc.WithBlock())
+	conn, err := grpc.DialContext(ctx, *apiServerAddrPtr, grpc.WithInsecure(), grpc.WithBlock())
 	if err != nil {
-		return newError("failed to dial ", *serverAddrPtr).Base(err)
+		base.Fatalf("failed to dial %s", *apiServerAddrPtr)
 	}
 	defer conn.Close()
 
 	response, err := handler(ctx, conn, method, unnamedArgs[1])
 	if err != nil {
-		return newError("failed to call service ", unnamedArgs[0]).Base(err)
+		base.Fatalf("failed to call service %s: %s", unnamedArgs[0], err)
 	}
 
 	fmt.Println(response)
-	return nil
 }
 
 func getServiceMethod(s string) (string, string) {
@@ -103,9 +97,6 @@ func callLogService(ctx context.Context, conn *grpc.ClientConn, method string, r
 	switch strings.ToLower(method) {
 	case "restartlogger":
 		r := &logService.RestartLoggerRequest{}
-		if err := proto.UnmarshalText(request, r); err != nil {
-			return "", err
-		}
 		resp, err := client.RestartLogger(ctx, r)
 		if err != nil {
 			return "", err
@@ -152,7 +143,3 @@ func callStatsService(ctx context.Context, conn *grpc.ClientConn, method string,
 		return "", errors.New("Unknown method: " + method)
 	}
 }
-
-func init() {
-	common.Must(RegisterCommand(&APICommand{}))
-}

+ 6 - 8
infra/control/certchainhash.go → commands/all/certchainhash.go

@@ -1,10 +1,11 @@
-package control
+package all
 
 import (
 	"flag"
 	"fmt"
 	"io/ioutil"
 
+	"github.com/v2fly/v2ray-core/v4/commands/base"
 	v2tls "github.com/v2fly/v2ray-core/v4/transport/internet/tls"
 )
 
@@ -14,13 +15,10 @@ func (c CertificateChainHashCommand) Name() string {
 	return "certChainHash"
 }
 
-func (c CertificateChainHashCommand) Description() Description {
-	return Description{
-		Short: "Calculate TLS certificates hash.",
-		Usage: []string{
-			"v2ctl certChainHash --cert <cert.pem>",
-			"Calculate TLS certificate chain hash.",
-		},
+func (c CertificateChainHashCommand) Description() base.Command {
+	return base.Command{
+		Short:     "Calculate TLS certificates hash.",
+		UsageLine: "v2ctl certChainHash --cert <cert.pem Calculate TLS certificate chain hash.",
 	}
 }
 

+ 17 - 0
commands/all/commands.go

@@ -0,0 +1,17 @@
+package all
+
+import "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,
+		cmdConvert,
+		cmdLove,
+		cmdTLS,
+		cmdUUID,
+		cmdVerify,
+	)
+}

+ 126 - 0
commands/all/convert.go

@@ -0,0 +1,126 @@
+package all
+
+import (
+	"bytes"
+	"fmt"
+	"io"
+	"io/ioutil"
+	"net/http"
+	"net/url"
+	"os"
+	"strings"
+	"time"
+
+	"github.com/v2fly/v2ray-core/v4/commands/base"
+	"github.com/v2fly/v2ray-core/v4/common"
+	"github.com/v2fly/v2ray-core/v4/common/buf"
+	"github.com/v2fly/v2ray-core/v4/infra/conf"
+	"github.com/v2fly/v2ray-core/v4/infra/conf/serial"
+	"google.golang.org/protobuf/proto"
+)
+
+var cmdConvert = &base.Command{
+	UsageLine: "{{.Exec}} convert [json file] [json file] ...",
+	Short:     "Convert multiple json config to protobuf",
+	Long: `
+Convert multiple json config to protobuf.
+
+Examples:
+
+    {{.Exec}} {{.LongName}} config.json c1.json c2.json <url>.json
+`,
+}
+
+func init() {
+	cmdConvert.Run = executeConvert // break init loop
+}
+
+func executeConvert(cmd *base.Command, args []string) {
+	unnamedArgs := cmdConvert.Flag.Args()
+	if len(unnamedArgs) < 1 {
+		base.Fatalf("empty config list")
+	}
+
+	conf := &conf.Config{}
+	for _, arg := range unnamedArgs {
+		fmt.Fprintf(os.Stderr, "Read config: %s", arg)
+		r, err := loadArg(arg)
+		common.Must(err)
+		c, err := serial.DecodeJSONConfig(r)
+		if err != nil {
+			base.Fatalf(err.Error())
+		}
+		conf.Override(c, arg)
+	}
+
+	pbConfig, err := conf.Build()
+	if err != nil {
+		base.Fatalf(err.Error())
+	}
+
+	bytesConfig, err := proto.Marshal(pbConfig)
+	if err != nil {
+		base.Fatalf("failed to marshal proto config: %s", err)
+	}
+
+	if _, err := os.Stdout.Write(bytesConfig); err != nil {
+		base.Fatalf("failed to write proto config: %s", err)
+	}
+}
+
+// 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, newError("invalid URL: ", target).Base(err)
+	}
+
+	if s := strings.ToLower(parsedTarget.Scheme); s != "http" && s != "https" {
+		return nil, newError("invalid scheme: ", 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, newError("failed to dial to ", target).Base(err)
+	}
+	defer resp.Body.Close()
+
+	if resp.StatusCode != 200 {
+		return nil, newError("unexpected HTTP status code: ", resp.StatusCode)
+	}
+
+	content, err := buf.ReadAllToBytes(resp.Body)
+	if err != nil {
+		return nil, newError("failed to read HTTP response").Base(err)
+	}
+
+	return content, nil
+}

+ 1 - 1
infra/control/errors.generated.go → commands/all/errors.generated.go

@@ -1,4 +1,4 @@
-package control
+package all
 
 import "github.com/v2fly/v2ray-core/v4/common/errors"
 

+ 37 - 0
commands/all/love.go

@@ -0,0 +1,37 @@
+package all
+
+import (
+	"bufio"
+	"bytes"
+	"compress/gzip"
+	"encoding/base64"
+	"fmt"
+
+	"github.com/v2fly/v2ray-core/v4/commands/base"
+	"github.com/v2fly/v2ray-core/v4/common"
+	"github.com/v2fly/v2ray-core/v4/common/platform"
+)
+
+var cmdLove = &base.Command{
+	UsageLine: "{{.Exec}} lovevictoria",
+	Short:     "", // set Short to "" hides the command
+	Long:      "",
+	Run:       executeLove,
+}
+
+func executeLove(cmd *base.Command, args []string) {
+	const content = "H4sIAAAAAAAC/4SVMaskNwzH+/kUW6izcSthMGrcqLhVk0rdQS5cSMg7Xu4S0vizB8meZd57M3ta2GHX/ukvyZZmY2ZKDMzCzJyY5yOlxKII1omsf+qkBiiC6WhbYsbkjDAfySQsJqD3jtrD0EBM3sBHzG3kUsrglIQREXonpd47kYIi4AHmgI9Wcq2jlJITC6JZJ+v3ECYzBMAHyYm392yuY4zWsjACmHZSh6l3A0JETzGlWZqDsnArpTg62mhJONhOdO90p97V1BAnteoaOcuummtrrtuERQwUiJwP8a4KGKcyxdOCw1spOY+WHueFqmakAIgUSSuhwKNgobxKXSLbtg6r5cFmBiAeF6yCkYycmv+BiCIiW8ScHa3DgxAuZQbRhFNrLTFo96RBmx9jKWWG5nBsjyJzuIkftUblonppZU5t5LzwIks5L1a4lijagQxLokbIYwxfytNDC+XQqrWW9fzAunhqh5/Tg8PuaMw0d/Tcw3iDO81bHfWM/AnutMh2xqSUntMzd3wHDy9iHMQz8bmUZYvqedTJ5GgOnrNt7FIbSlwXE3wDI19n/KA38MsLaP4l89b5F8AV3ESOMIEhIBgezHBc0H6xV9KbaXwMvPcNvIHcC0C7UPZQx4JVTb35/AneSQq+bAYXsBmY7TCRupF2NTdVm/+ch22xa0pvRERKqt1oxj9DUbXzU84Gvj5hc5a81SlAUwMwgEs4T9+7sg9lb9h+908MWiKV8xtWciVTmnB3tivRjNerfXdxpfEBbq2NUvLMM5R9NLuyQg8nXT0PIh1xPd/wrcV49oJ6zbZaPlj2V87IY9T3F2XCOcW2MbZyZd49H+9m81E1N9SxlU+ff/1y+/f3719vf7788+Ugv/ffbMIH7ZNj0dsT4WMHHwLPu/Rp2O75uh99AK+N2xn7ZHq1OK6gczkN+9ngdOl1Qvki5xwSR8vFX6D+9vXA97B/+fr5rz9u/738uP328urP19vfP759e3n9Xs6jamvqlfJ/AAAA//+YAMZjDgkAAA=="
+	c, err := base64.StdEncoding.DecodeString(content)
+	common.Must(err)
+	reader, err := gzip.NewReader(bytes.NewBuffer(c))
+	common.Must(err)
+	b := make([]byte, 4096)
+	nBytes, _ := reader.Read(b)
+
+	bb := bytes.NewBuffer(b[:nBytes])
+	scanner := bufio.NewScanner(bb)
+	for scanner.Scan() {
+		s := scanner.Text()
+		fmt.Print(s + platform.LineSeparator())
+	}
+}

+ 18 - 0
commands/all/tls.go

@@ -0,0 +1,18 @@
+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,
+	},
+}

+ 143 - 0
commands/all/tlscmd/cert.go

@@ -0,0 +1,143 @@
+package tlscmd
+
+import (
+	"context"
+	"crypto/x509"
+	"encoding/json"
+	"os"
+	"strings"
+	"time"
+
+	"github.com/v2fly/v2ray-core/v4/commands/base"
+	"github.com/v2fly/v2ray-core/v4/common"
+	"github.com/v2fly/v2ray-core/v4/common/protocol/tls/cert"
+	"github.com/v2fly/v2ray-core/v4/common/task"
+)
+
+// 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: `
+Generate TLS certificates.
+
+Arguments:
+
+	-domain=domain_name 
+		The domain name for the certificate.
+
+	-org=organization 
+		The organization name for the certificate.
+
+	-ca 
+		Whether this certificate is a CA
+
+	-json 
+		The output of certificate to JSON
+
+	-file 
+		The certificate path to save.
+
+	-expire 
+		Expire time of the certificate. Default value 3 months.
+`,
+}
+
+func init() {
+	CmdCert.Run = executeCert // break init loop
+}
+
+var (
+	certDomainNames stringList
+	_               = func() bool {
+		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.")
+)
+
+func executeCert(cmd *base.Command, args []string) {
+	var opts []cert.Option
+	if *certIsCA {
+		opts = append(opts, cert.Authority(*certIsCA))
+		opts = append(opts, cert.KeyUsage(x509.KeyUsageCertSign|x509.KeyUsageKeyEncipherment|x509.KeyUsageDigitalSignature))
+	}
+
+	opts = append(opts, cert.NotAfter(time.Now().Add(*certExpire)))
+	opts = append(opts, cert.CommonName(*certCommonName))
+	if len(certDomainNames) > 0 {
+		opts = append(opts, cert.DNSNames(certDomainNames...))
+	}
+	opts = append(opts, cert.Organization(*certOrganization))
+
+	cert, err := cert.Generate(nil, opts...)
+	if err != nil {
+		base.Fatalf("failed to generate TLS certificate: %s", err)
+	}
+
+	if *certJSONOutput {
+		printJSON(cert)
+	}
+
+	if len(*certFileOutput) > 0 {
+		if err := printFile(cert, *certFileOutput); err != nil {
+			base.Fatalf("failed to save file: %s", err)
+		}
+	}
+}
+
+func printJSON(certificate *cert.Certificate) {
+	certPEM, keyPEM := certificate.ToPEM()
+	jCert := &jsonCert{
+		Certificate: strings.Split(strings.TrimSpace(string(certPEM)), "\n"),
+		Key:         strings.Split(strings.TrimSpace(string(keyPEM)), "\n"),
+	}
+	content, err := json.MarshalIndent(jCert, "", "  ")
+	common.Must(err)
+	os.Stdout.Write(content)
+	os.Stdout.WriteString("\n")
+}
+
+func writeFile(content []byte, name string) error {
+	f, err := os.Create(name)
+	if err != nil {
+		return err
+	}
+	defer f.Close()
+
+	return common.Error2(f.Write(content))
+}
+
+func printFile(certificate *cert.Certificate, name string) error {
+	certPEM, keyPEM := certificate.ToPEM()
+	return task.Run(context.Background(), func() error {
+		return writeFile(certPEM, name+"_cert.pem")
+	}, func() error {
+		return writeFile(keyPEM, name+"_key.pem")
+	})
+}
+
+type stringList []string
+
+func (l *stringList) String() string {
+	return "String list"
+}
+
+func (l *stringList) Set(v string) error {
+	if v == "" {
+		base.Fatalf("empty value")
+	}
+	*l = append(*l, v)
+	return nil
+}
+
+type jsonCert struct {
+	Certificate []string `json:"certificate"`
+	Key         []string `json:"key"`
+}

+ 35 - 40
infra/control/tlsping.go → commands/all/tlscmd/ping.go

@@ -1,65 +1,57 @@
-package control
+package tlscmd
 
 import (
 	"crypto/tls"
 	"crypto/x509"
 	"encoding/base64"
-	"flag"
 	"fmt"
 	"net"
 
-	"github.com/v2fly/v2ray-core/v4/common"
+	"github.com/v2fly/v2ray-core/v4/commands/base"
 	v2tls "github.com/v2fly/v2ray-core/v4/transport/internet/tls"
 )
 
-type TLSPingCommand struct{}
+// CmdPing is the tls ping command
+var CmdPing = &base.Command{
+	UsageLine: "{{.Exec}} tls ping [-ip <ip>] <domain>",
+	Short:     "Ping the domain with TLS handshake",
+	Long: `
+Ping the domain with TLS handshake.
 
-func (c *TLSPingCommand) Name() string {
-	return "tlsping"
-}
+Arguments:
 
-func (c *TLSPingCommand) Description() Description {
-	return Description{
-		Short: "Ping the domain with TLS handshake",
-		Usage: []string{"v2ctl tlsping <domain> --ip <ip>"},
-	}
+	-ip
+		The IP address of the domain.
+`,
 }
 
-func printCertificates(certs []*x509.Certificate) {
-	for _, cert := range certs {
-		if len(cert.DNSNames) == 0 {
-			continue
-		}
-		fmt.Println("Allowed domains: ", cert.DNSNames)
-	}
+func init() {
+	CmdPing.Run = executePing // break init loop
 }
 
-func (c *TLSPingCommand) Execute(args []string) error {
-	fs := flag.NewFlagSet(c.Name(), flag.ContinueOnError)
-	ipStr := fs.String("ip", "", "IP address of the domain")
-
-	if err := fs.Parse(args); err != nil {
-		return newError("flag parsing").Base(err)
-	}
+var (
+	pingIPStr = CmdPing.Flag.String("ip", "", "")
+)
 
-	if fs.NArg() < 1 {
-		return newError("domain not specified")
+func executePing(cmd *base.Command, args []string) {
+	if CmdPing.Flag.NArg() < 1 {
+		base.Fatalf("domain not specified")
 	}
 
-	domain := fs.Arg(0)
+	domain := CmdPing.Flag.Arg(0)
 	fmt.Println("Tls ping: ", domain)
 
 	var ip net.IP
-	if len(*ipStr) > 0 {
-		v := net.ParseIP(*ipStr)
+	if len(*pingIPStr) > 0 {
+		v := net.ParseIP(*pingIPStr)
 		if v == nil {
-			return newError("invalid IP: ", *ipStr)
+			base.Fatalf("invalid IP: %s", *pingIPStr)
 		}
 		ip = v
 	} else {
 		v, err := net.ResolveIPAddr("ip", domain)
 		if err != nil {
-			return newError("resolve IP").Base(err)
+			base.Fatalf("Failed to resolve IP: %s", err)
 		}
 		ip = v.IP
 	}
@@ -70,7 +62,7 @@ func (c *TLSPingCommand) Execute(args []string) error {
 	{
 		tcpConn, err := net.DialTCP("tcp", nil, &net.TCPAddr{IP: ip, Port: 443})
 		if err != nil {
-			return newError("dial tcp").Base(err)
+			base.Fatalf("Failed to dial tcp: %s", err)
 		}
 		tlsConn := tls.Client(tcpConn, &tls.Config{
 			InsecureSkipVerify: true,
@@ -95,7 +87,7 @@ func (c *TLSPingCommand) Execute(args []string) error {
 	{
 		tcpConn, err := net.DialTCP("tcp", nil, &net.TCPAddr{IP: ip, Port: 443})
 		if err != nil {
-			return newError("dial tcp").Base(err)
+			base.Fatalf("Failed to dial tcp: %s", err)
 		}
 		tlsConn := tls.Client(tcpConn, &tls.Config{
 			ServerName: domain,
@@ -116,8 +108,15 @@ func (c *TLSPingCommand) Execute(args []string) error {
 	}
 
 	fmt.Println("Tls ping finished")
+}
 
-	return nil
+func printCertificates(certs []*x509.Certificate) {
+	for _, cert := range certs {
+		if len(cert.DNSNames) == 0 {
+			continue
+		}
+		fmt.Println("Allowed domains: ", cert.DNSNames)
+	}
 }
 
 func showCert() func(rawCerts [][]byte, verifiedChains [][]*x509.Certificate) error {
@@ -127,7 +126,3 @@ func showCert() func(rawCerts [][]byte, verifiedChains [][]*x509.Certificate) er
 		return nil
 	}
 }
-
-func init() {
-	common.Must(RegisterCommand(&TLSPingCommand{}))
-}

+ 21 - 0
commands/all/uuid.go

@@ -0,0 +1,21 @@
+package all
+
+import (
+	"fmt"
+
+	"github.com/v2fly/v2ray-core/v4/commands/base"
+	"github.com/v2fly/v2ray-core/v4/common/uuid"
+)
+
+var cmdUUID = &base.Command{
+	UsageLine: "{{.Exec}} uuid",
+	Short:     "Generate new UUIDs",
+	Long: `Generate new UUIDs.
+`,
+	Run: executeUUID,
+}
+
+func executeUUID(cmd *base.Command, args []string) {
+	u := uuid.New()
+	fmt.Println(u.String())
+}

+ 53 - 0
commands/all/verify.go

@@ -0,0 +1,53 @@
+package all
+
+import (
+	"os"
+
+	"github.com/v2fly/VSign/signerVerify"
+	"github.com/v2fly/v2ray-core/v4/commands/base"
+)
+
+var cmdVerify = &base.Command{
+	UsageLine: "{{.Exec}} verify [--sig=sig-file] file",
+	Short:     "Verify if a binary is officially signed",
+	Long: `
+Verify if a binary is officially signed.
+
+Arguments:
+
+	-sig 
+		The path to the signature file
+`,
+}
+
+func init() {
+	cmdVerify.Run = executeVerify // break init loop
+}
+
+var (
+	verifySigFile = cmdVerify.Flag.String("sig", "", "Path to the signature file")
+)
+
+func executeVerify(cmd *base.Command, args []string) {
+	target := cmdVerify.Flag.Arg(0)
+	if target == "" {
+		base.Fatalf("empty file path.")
+	}
+
+	if *verifySigFile == "" {
+		base.Fatalf("empty signature path.")
+	}
+
+	sigReader, err := os.Open(os.ExpandEnv(*verifySigFile))
+	if err != nil {
+		base.Fatalf("failed to open file %s: %s ", *verifySigFile, err)
+	}
+
+	files := cmdVerify.Flag.Args()
+
+	err = signerVerify.OutputAndJudge(signerVerify.CheckSignaturesV2Fly(sigReader, files))
+
+	if err != nil {
+		base.Fatalf("file is not officially signed by V2Ray: %s", err)
+	}
+}

+ 122 - 0
commands/base/command.go

@@ -0,0 +1,122 @@
+// Copyright 2017 The Go Authors. All rights reserved.
+// Use of this source code is governed by a BSD-style
+// license that can be found in the LICENSE file.
+
+// Package base defines shared basic pieces of the commands,
+// in particular logging and the Command structure.
+package base
+
+import (
+	"flag"
+	"fmt"
+	"os"
+	"strings"
+	"sync"
+)
+
+// A Command is an implementation of a v2ray command
+// like v2ray run or v2ray version.
+type Command struct {
+	// Run runs the command.
+	// The args are the arguments after the command name.
+	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.
+	UsageLine string
+
+	// Short is the short description shown in the 'go help' output.
+	Short string
+
+	// Long is the long message shown in the 'go help <this-command>' output.
+	Long string
+
+	// Flag is a set of flags specific to this command.
+	Flag flag.FlagSet
+
+	// CustomFlags indicates that the command will do its own
+	// flag parsing.
+	CustomFlags bool
+
+	// Commands lists the available commands and help topics.
+	// The order here is the order in which they are printed by 'go help'.
+	// Note that subcommands are in general best avoided.
+	Commands []*Command
+}
+
+// LongName returns the command's long name: all the words in the usage line between "go" and a flag or argument,
+func (c *Command) LongName() string {
+	name := c.UsageLine
+	if i := strings.Index(name, " ["); i >= 0 {
+		name = name[:i]
+	}
+	if name == CommandEnv.Exec {
+		return ""
+	}
+	return strings.TrimPrefix(name, CommandEnv.Exec+" ")
+}
+
+// Name returns the command's short name: the last word in the usage line before a flag or argument.
+func (c *Command) Name() string {
+	name := c.LongName()
+	if i := strings.LastIndex(name, " "); i >= 0 {
+		name = name[i+1:]
+	}
+	return name
+}
+
+// Usage prints usage of the Command
+func (c *Command) Usage() {
+	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)
+	Exit()
+}
+
+// Runnable reports whether the command can be run; otherwise
+// it is a documentation pseudo-command such as importpath.
+func (c *Command) Runnable() bool {
+	return c.Run != nil
+}
+
+// Exit exits with code set with SetExitStatus()
+func Exit() {
+	os.Exit(exitStatus)
+}
+
+// Fatalf logs error and exit with code 1
+func Fatalf(format string, args ...interface{}) {
+	Errorf(format, args...)
+	Exit()
+}
+
+// Errorf logs error and set exit status to 1, but not exit
+func Errorf(format string, args ...interface{}) {
+	fmt.Fprintf(os.Stderr, format, args...)
+	fmt.Fprintln(os.Stderr)
+	SetExitStatus(1)
+}
+
+// ExitIfErrors exits if current status is not zero
+func ExitIfErrors() {
+	if exitStatus != 0 {
+		Exit()
+	}
+}
+
+var exitStatus = 0
+var exitMu sync.Mutex
+
+// SetExitStatus set exit status code
+func SetExitStatus(n int) {
+	exitMu.Lock()
+	if exitStatus < n {
+		exitStatus = n
+	}
+	exitMu.Unlock()
+}
+
+// GetExitStatus get exit status code
+func GetExitStatus() int {
+	return exitStatus
+}

+ 22 - 0
commands/base/env.go

@@ -0,0 +1,22 @@
+package base
+
+import (
+	"os"
+	"path"
+)
+
+// CommandEnvHolder is a struct holds the environment info of commands
+type CommandEnvHolder struct {
+	Exec string
+}
+
+// CommandEnv holds the environment info of commands
+var CommandEnv CommandEnvHolder
+
+func init() {
+	exec, err := os.Executable()
+	if err != nil {
+		return
+	}
+	CommandEnv.Exec = path.Base(exec)
+}

+ 88 - 0
commands/base/execute.go

@@ -0,0 +1,88 @@
+package base
+
+import (
+	"flag"
+	"fmt"
+	"os"
+	"sort"
+	"strings"
+)
+
+// Copyright 2011 The Go Authors. All rights reserved.
+// Use of this source code is governed by a BSD-style
+// copied from "github.com/golang/go/main.go"
+
+// Execute excute the commands
+func Execute() {
+	buildCommandsText(RootCommand)
+	flag.Parse()
+	args := flag.Args()
+	if len(args) < 1 {
+		PrintUsage(os.Stderr, RootCommand)
+		return
+	}
+	cmdName := args[0] // for error messages
+	if args[0] == "help" {
+		Help(os.Stdout, args[1:])
+		return
+	}
+
+BigCmdLoop:
+	for bigCmd := RootCommand; ; {
+		for _, cmd := range bigCmd.Commands {
+			if cmd.Name() != args[0] {
+				continue
+			}
+			if len(cmd.Commands) > 0 {
+				// test sub commands
+				bigCmd = cmd
+				args = args[1:]
+				if len(args) == 0 {
+					PrintUsage(os.Stderr, bigCmd)
+					SetExitStatus(2)
+					Exit()
+				}
+				if args[0] == "help" {
+					// Accept 'go mod help' and 'go mod help foo' for 'go help mod' and 'go help mod foo'.
+					Help(os.Stdout, append(strings.Split(cmdName, " "), args[1:]...))
+					return
+				}
+				cmdName += " " + args[0]
+				continue BigCmdLoop
+			}
+			if !cmd.Runnable() {
+				continue
+			}
+			cmd.Flag.Usage = func() { cmd.Usage() }
+			if cmd.CustomFlags {
+				args = args[1:]
+			} else {
+				cmd.Flag.Parse(args[1:])
+				args = cmd.Flag.Args()
+			}
+
+			cmd.Run(cmd, args)
+			Exit()
+			return
+		}
+		helpArg := ""
+		if i := strings.LastIndex(cmdName, " "); i >= 0 {
+			helpArg = " " + cmdName[:i]
+		}
+		fmt.Fprintf(os.Stderr, "%s %s: unknown command\nRun '%s help%s' for usage.\n", CommandEnv.Exec, cmdName, CommandEnv.Exec, helpArg)
+		SetExitStatus(2)
+		Exit()
+	}
+}
+
+// SortCommands sorts the first level sub commands
+func SortCommands() {
+	sort.Slice(RootCommand.Commands, func(i, j int) bool {
+		return SortLessFunc(RootCommand.Commands[i], RootCommand.Commands[j])
+	})
+}
+
+// SortLessFunc used for sort commands list, can be override from outside
+var SortLessFunc = func(i, j *Command) bool {
+	return i.Name() < j.Name()
+}

+ 158 - 0
commands/base/help.go

@@ -0,0 +1,158 @@
+// Copyright 2017 The Go Authors. All rights reserved.
+// Use of this source code is governed by a BSD-style
+// license that can be found in the LICENSE file.
+
+package base
+
+import (
+	"bufio"
+	"bytes"
+	"fmt"
+	"io"
+	"os"
+	"strings"
+	"text/template"
+	"unicode"
+	"unicode/utf8"
+)
+
+// Help implements the 'help' command.
+func Help(w io.Writer, args []string) {
+	cmd := RootCommand
+Args:
+	for i, arg := range args {
+		for _, sub := range cmd.Commands {
+			if sub.Name() == arg {
+				cmd = sub
+				continue Args
+			}
+		}
+
+		// helpSuccess is the help command using as many args as possible that would succeed.
+		helpSuccess := CommandEnv.Exec + " help"
+		if i > 0 {
+			helpSuccess += " " + strings.Join(args[:i], " ")
+		}
+		fmt.Fprintf(os.Stderr, "%s help %s: unknown help topic. Run '%s'.\n", CommandEnv.Exec, strings.Join(args, " "), helpSuccess)
+		SetExitStatus(2) // failed at 'v2ray help cmd'
+		Exit()
+	}
+
+	if len(cmd.Commands) > 0 {
+		PrintUsage(os.Stdout, cmd)
+	} else {
+		tmpl(os.Stdout, helpTemplate, makeTmplData(cmd))
+	}
+}
+
+var usageTemplate = `{{.Long | trim}}
+
+Usage:
+
+	{{.UsageLine}} <command> [arguments]
+
+The commands are:
+{{range .Commands}}{{if and (ne .Short "") (or (.Runnable) .Commands)}}
+	{{.Name | printf "%-12s"}} {{.Short}}{{end}}{{end}}
+
+Use "{{.Exec}} help{{with .LongName}} {{.}}{{end}} <command>" for more information about a command.
+`
+
+// APPEND FOLLOWING TO 'usageTemplate' IF YOU WANT DOC,
+// A DOC TOPIC IS JUST A COMMAND NOT RUNNABLE:
+//
+// {{if eq (.UsageLine) (.Exec)}}
+// Additional help topics:
+// {{range .Commands}}{{if and (not .Runnable) (not .Commands)}}
+// 	{{.Name | printf "%-15s"}} {{.Short}}{{end}}{{end}}
+//
+// Use "{{.Exec}} help{{with .LongName}} {{.}}{{end}} <topic>" for more information about that topic.
+// {{end}}
+
+var helpTemplate = `{{if .Runnable}}usage: {{.UsageLine}}
+
+{{end}}{{.Long | trim}}
+`
+
+// An errWriter wraps a writer, recording whether a write error occurred.
+type errWriter struct {
+	w   io.Writer
+	err error
+}
+
+func (w *errWriter) Write(b []byte) (int, error) {
+	n, err := w.w.Write(b)
+	if err != nil {
+		w.err = err
+	}
+	return n, err
+}
+
+// tmpl executes the given template text on data, writing the result to w.
+func tmpl(w io.Writer, text string, data interface{}) {
+	t := template.New("top")
+	t.Funcs(template.FuncMap{"trim": strings.TrimSpace, "capitalize": capitalize})
+	template.Must(t.Parse(text))
+	ew := &errWriter{w: w}
+	err := t.Execute(ew, data)
+	if ew.err != nil {
+		// I/O error writing. Ignore write on closed pipe.
+		if strings.Contains(ew.err.Error(), "pipe") {
+			SetExitStatus(1)
+			Exit()
+		}
+		Fatalf("writing output: %v", ew.err)
+	}
+	if err != nil {
+		panic(err)
+	}
+}
+
+func capitalize(s string) string {
+	if s == "" {
+		return s
+	}
+	r, n := utf8.DecodeRuneInString(s)
+	return string(unicode.ToTitle(r)) + s[n:]
+}
+
+// PrintUsage prints usage of cmd to w
+func PrintUsage(w io.Writer, cmd *Command) {
+	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))
+}
+
+func buildText(text string, data interface{}) string {
+	buf := bytes.NewBuffer([]byte{})
+	text = strings.ReplaceAll(text, "\t", "    ")
+	tmpl(buf, text, data)
+	return buf.String()
+}
+
+type tmplData struct {
+	*Command
+	*CommandEnvHolder
+}
+
+func makeTmplData(cmd *Command) tmplData {
+	return tmplData{
+		Command:          cmd,
+		CommandEnvHolder: &CommandEnv,
+	}
+}

+ 16 - 0
commands/base/root.go

@@ -0,0 +1,16 @@
+package base
+
+// RootCommand is the root command of all commands
+var RootCommand *Command
+
+func init() {
+	RootCommand = &Command{
+		UsageLine: CommandEnv.Exec,
+		Long:      "The root command",
+	}
+}
+
+// RegisterCommand register a command to RootCommand
+func RegisterCommand(cmd *Command) {
+	RootCommand.Commands = append(RootCommand.Commands, cmd)
+}

+ 16 - 7
config.go

@@ -62,15 +62,24 @@ func getExtension(filename string) string {
 // * []string slice of multiple filename/url(s) to open to read
 // * io.Reader that reads a config content (the original way)
 func LoadConfig(formatName string, filename string, input interface{}) (*Config, error) {
-	ext := getExtension(filename)
-	if len(ext) > 0 {
-		if f, found := configLoaderByExt[ext]; found {
+	if formatName != "" {
+		// if clearly specified, we can safely assume that user knows what they are
+		if f, found := configLoaderByName[formatName]; found {
+			return f.Loader(input)
+		}
+	} else {
+		// no explicitly specified loader, extenstion detect first
+		ext := getExtension(filename)
+		if len(ext) > 0 {
+			if f, found := configLoaderByExt[ext]; found {
+				return f.Loader(input)
+			}
+		}
+		// try default loader
+		formatName = "json"
+		if f, found := configLoaderByName[formatName]; found {
 			return f.Loader(input)
 		}
-	}
-
-	if f, found := configLoaderByName[formatName]; found {
-		return f.Loader(input)
 	}
 
 	return nil, newError("Unable to load config in ", formatName).AtWarning()

+ 0 - 49
infra/conf/command/command.go

@@ -1,49 +0,0 @@
-package command
-
-//go:generate go run github.com/v2fly/v2ray-core/v4/common/errors/errorgen
-
-import (
-	"os"
-
-	"google.golang.org/protobuf/proto"
-
-	"github.com/v2fly/v2ray-core/v4/common"
-	"github.com/v2fly/v2ray-core/v4/infra/conf/serial"
-	"github.com/v2fly/v2ray-core/v4/infra/control"
-)
-
-type ConfigCommand struct{}
-
-func (c *ConfigCommand) Name() string {
-	return "config"
-}
-
-func (c *ConfigCommand) Description() control.Description {
-	return control.Description{
-		Short: "Convert config among different formats.",
-		Usage: []string{
-			"v2ctl config",
-		},
-	}
-}
-
-func (c *ConfigCommand) Execute(args []string) error {
-	pbConfig, err := serial.LoadJSONConfig(os.Stdin)
-	if err != nil {
-		return newError("failed to parse json config").Base(err)
-	}
-
-	bytesConfig, err := proto.Marshal(pbConfig)
-	if err != nil {
-		return newError("failed to marshal proto config").Base(err)
-	}
-
-	if _, err := os.Stdout.Write(bytesConfig); err != nil {
-		return newError("failed to write proto config").Base(err)
-	}
-	return nil
-}
-
-func init() {
-	common.Must(control.RegisterCommand(&ConfigCommand{}))
-}

+ 0 - 138
infra/control/cert.go

@@ -1,138 +0,0 @@
-package control
-
-import (
-	"context"
-	"crypto/x509"
-	"encoding/json"
-	"flag"
-	"os"
-	"strings"
-	"time"
-
-	"github.com/v2fly/v2ray-core/v4/common"
-	"github.com/v2fly/v2ray-core/v4/common/protocol/tls/cert"
-	"github.com/v2fly/v2ray-core/v4/common/task"
-)
-
-type stringList []string
-
-func (l *stringList) String() string {
-	return "String list"
-}
-
-func (l *stringList) Set(v string) error {
-	if v == "" {
-		return newError("empty value")
-	}
-	*l = append(*l, v)
-	return nil
-}
-
-type jsonCert struct {
-	Certificate []string `json:"certificate"`
-	Key         []string `json:"key"`
-}
-
-type CertificateCommand struct{}
-
-func (c *CertificateCommand) Name() string {
-	return "cert"
-}
-
-func (c *CertificateCommand) Description() Description {
-	return Description{
-		Short: "Generate TLS certificates.",
-		Usage: []string{
-			"v2ctl cert [--ca] [--domain=v2fly.org] [--expire=240h]",
-			"Generate new TLS certificate",
-			"--ca The new certificate is a CA certificate",
-			"--domain Common name for the certificate",
-			"--expire Time until certificate expires. 240h = 10 days.",
-		},
-	}
-}
-
-func (c *CertificateCommand) printJSON(certificate *cert.Certificate) {
-	certPEM, keyPEM := certificate.ToPEM()
-	jCert := &jsonCert{
-		Certificate: strings.Split(strings.TrimSpace(string(certPEM)), "\n"),
-		Key:         strings.Split(strings.TrimSpace(string(keyPEM)), "\n"),
-	}
-	content, err := json.MarshalIndent(jCert, "", "  ")
-	common.Must(err)
-	os.Stdout.Write(content)
-	os.Stdout.WriteString("\n")
-}
-
-func (c *CertificateCommand) writeFile(content []byte, name string) error {
-	f, err := os.Create(name)
-	if err != nil {
-		return err
-	}
-	defer f.Close()
-
-	return common.Error2(f.Write(content))
-}
-
-func (c *CertificateCommand) printFile(certificate *cert.Certificate, name string) error {
-	certPEM, keyPEM := certificate.ToPEM()
-	return task.Run(context.Background(), func() error {
-		return c.writeFile(certPEM, name+"_cert.pem")
-	}, func() error {
-		return c.writeFile(keyPEM, name+"_key.pem")
-	})
-}
-
-func (c *CertificateCommand) Execute(args []string) error {
-	fs := flag.NewFlagSet(c.Name(), flag.ContinueOnError)
-
-	var domainNames stringList
-	fs.Var(&domainNames, "domain", "Domain name for the certificate")
-
-	commonName := fs.String("name", "V2Ray Inc", "The common name of this certificate")
-	organization := fs.String("org", "V2Ray Inc", "Organization of the certificate")
-
-	isCA := fs.Bool("ca", false, "Whether this certificate is a CA")
-	jsonOutput := fs.Bool("json", true, "Print certificate in JSON format")
-	fileOutput := fs.String("file", "", "Save certificate in file.")
-
-	expire := fs.Duration("expire", time.Hour*24*90 /* 90 days */, "Time until the certificate expires. Default value 3 months.")
-
-	if err := fs.Parse(args); err != nil {
-		return err
-	}
-
-	var opts []cert.Option
-	if *isCA {
-		opts = append(opts, cert.Authority(*isCA))
-		opts = append(opts, cert.KeyUsage(x509.KeyUsageCertSign|x509.KeyUsageKeyEncipherment|x509.KeyUsageDigitalSignature))
-	}
-
-	opts = append(opts, cert.NotAfter(time.Now().Add(*expire)))
-	opts = append(opts, cert.CommonName(*commonName))
-	if len(domainNames) > 0 {
-		opts = append(opts, cert.DNSNames(domainNames...))
-	}
-	opts = append(opts, cert.Organization(*organization))
-
-	cert, err := cert.Generate(nil, opts...)
-	if err != nil {
-		return newError("failed to generate TLS certificate").Base(err)
-	}
-
-	if *jsonOutput {
-		c.printJSON(cert)
-	}
-
-	if len(*fileOutput) > 0 {
-		if err := c.printFile(cert, *fileOutput); err != nil {
-			return err
-		}
-	}
-
-	return nil
-}
-
-func init() {
-	common.Must(RegisterCommand(&CertificateCommand{}))
-}

+ 0 - 54
infra/control/command.go

@@ -1,54 +0,0 @@
-package control
-
-import (
-	"fmt"
-	"log"
-	"os"
-	"strings"
-)
-
-type Description struct {
-	Short string
-	Usage []string
-}
-
-type Command interface {
-	Name() string
-	Description() Description
-	Execute(args []string) error
-}
-
-var (
-	commandRegistry = make(map[string]Command)
-	ctllog          = log.New(os.Stderr, "v2ctl> ", 0)
-)
-
-func RegisterCommand(cmd Command) error {
-	entry := strings.ToLower(cmd.Name())
-	if entry == "" {
-		return newError("empty command name")
-	}
-	commandRegistry[entry] = cmd
-	return nil
-}
-
-func GetCommand(name string) Command {
-	cmd, found := commandRegistry[name]
-	if !found {
-		return nil
-	}
-	return cmd
-}
-
-type hiddenCommand interface {
-	Hidden() bool
-}
-
-func PrintUsage() {
-	for name, cmd := range commandRegistry {
-		if _, ok := cmd.(hiddenCommand); ok {
-			continue
-		}
-		fmt.Println("   ", name, "\t\t\t", cmd.Description())
-	}
-}

+ 0 - 91
infra/control/config.go

@@ -1,91 +0,0 @@
-package control
-
-import (
-	"bytes"
-	"io"
-	"io/ioutil"
-	"os"
-	"strings"
-
-	"github.com/golang/protobuf/proto"
-
-	"github.com/v2fly/v2ray-core/v4/common"
-	"github.com/v2fly/v2ray-core/v4/infra/conf"
-	"github.com/v2fly/v2ray-core/v4/infra/conf/serial"
-)
-
-// ConfigCommand is the json to pb convert struct
-type ConfigCommand struct{}
-
-// Name for cmd usage
-func (c *ConfigCommand) Name() string {
-	return "config"
-}
-
-// Description for help usage
-func (c *ConfigCommand) Description() Description {
-	return Description{
-		Short: "merge multiple json config",
-		Usage: []string{"v2ctl config config.json c1.json c2.json <url>.json"},
-	}
-}
-
-// Execute real work here.
-func (c *ConfigCommand) Execute(args []string) error {
-	if len(args) < 1 {
-		return newError("empty config list")
-	}
-
-	conf := &conf.Config{}
-	for _, arg := range args {
-		ctllog.Println("Read config: ", arg)
-		r, err := c.LoadArg(arg)
-		common.Must(err)
-		c, err := serial.DecodeJSONConfig(r)
-		if err != nil {
-			ctllog.Fatalln(err)
-		}
-		conf.Override(c, arg)
-	}
-
-	pbConfig, err := conf.Build()
-	if err != nil {
-		return err
-	}
-
-	bytesConfig, err := proto.Marshal(pbConfig)
-	if err != nil {
-		return newError("failed to marshal proto config").Base(err)
-	}
-
-	if _, err := os.Stdout.Write(bytesConfig); err != nil {
-		return newError("failed to write proto config").Base(err)
-	}
-
-	return nil
-}
-
-// LoadArg loads one arg, maybe an remote url, or local file path
-func (c *ConfigCommand) 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
-}
-
-func init() {
-	common.Must(RegisterCommand(&ConfigCommand{}))
-}

+ 0 - 3
infra/control/control.go

@@ -1,3 +0,0 @@
-package control
-
-//go:generate go run github.com/v2fly/v2ray-core/v4/common/errors/errorgen

+ 0 - 78
infra/control/fetch.go

@@ -1,78 +0,0 @@
-package control
-
-import (
-	"net/http"
-	"net/url"
-	"os"
-	"strings"
-	"time"
-
-	"github.com/v2fly/v2ray-core/v4/common"
-	"github.com/v2fly/v2ray-core/v4/common/buf"
-)
-
-type FetchCommand struct{}
-
-func (c *FetchCommand) Name() string {
-	return "fetch"
-}
-
-func (c *FetchCommand) Description() Description {
-	return Description{
-		Short: "Fetch resources",
-		Usage: []string{"v2ctl fetch <url>"},
-	}
-}
-
-func (c *FetchCommand) Execute(args []string) error {
-	if len(args) < 1 {
-		return newError("empty url")
-	}
-	content, err := FetchHTTPContent(args[0])
-	if err != nil {
-		return newError("failed to read HTTP response").Base(err)
-	}
-
-	os.Stdout.Write(content)
-	return nil
-}
-
-// FetchHTTPContent dials https for remote content
-func FetchHTTPContent(target string) ([]byte, error) {
-	parsedTarget, err := url.Parse(target)
-	if err != nil {
-		return nil, newError("invalid URL: ", target).Base(err)
-	}
-
-	if s := strings.ToLower(parsedTarget.Scheme); s != "http" && s != "https" {
-		return nil, newError("invalid scheme: ", 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, newError("failed to dial to ", target).Base(err)
-	}
-	defer resp.Body.Close()
-
-	if resp.StatusCode != 200 {
-		return nil, newError("unexpected HTTP status code: ", resp.StatusCode)
-	}
-
-	content, err := buf.ReadAllToBytes(resp.Body)
-	if err != nil {
-		return nil, newError("failed to read HTTP response").Base(err)
-	}
-
-	return content, nil
-}
-
-func init() {
-	common.Must(RegisterCommand(&FetchCommand{}))
-}

+ 0 - 53
infra/control/love.go

@@ -1,53 +0,0 @@
-package control
-
-import (
-	"bufio"
-	"bytes"
-	"compress/gzip"
-	"encoding/base64"
-	"fmt"
-
-	"github.com/v2fly/v2ray-core/v4/common"
-	"github.com/v2fly/v2ray-core/v4/common/platform"
-)
-
-const content = "H4sIAAAAAAAC/4SVMaskNwzH+/kUW6izcSthMGrcqLhVk0rdQS5cSMg7Xu4S0vizB8meZd57M3ta2GHX/ukvyZZmY2ZKDMzCzJyY5yOlxKII1omsf+qkBiiC6WhbYsbkjDAfySQsJqD3jtrD0EBM3sBHzG3kUsrglIQREXonpd47kYIi4AHmgI9Wcq2jlJITC6JZJ+v3ECYzBMAHyYm392yuY4zWsjACmHZSh6l3A0JETzGlWZqDsnArpTg62mhJONhOdO90p97V1BAnteoaOcuummtrrtuERQwUiJwP8a4KGKcyxdOCw1spOY+WHueFqmakAIgUSSuhwKNgobxKXSLbtg6r5cFmBiAeF6yCkYycmv+BiCIiW8ScHa3DgxAuZQbRhFNrLTFo96RBmx9jKWWG5nBsjyJzuIkftUblonppZU5t5LzwIks5L1a4lijagQxLokbIYwxfytNDC+XQqrWW9fzAunhqh5/Tg8PuaMw0d/Tcw3iDO81bHfWM/AnutMh2xqSUntMzd3wHDy9iHMQz8bmUZYvqedTJ5GgOnrNt7FIbSlwXE3wDI19n/KA38MsLaP4l89b5F8AV3ESOMIEhIBgezHBc0H6xV9KbaXwMvPcNvIHcC0C7UPZQx4JVTb35/AneSQq+bAYXsBmY7TCRupF2NTdVm/+ch22xa0pvRERKqt1oxj9DUbXzU84Gvj5hc5a81SlAUwMwgEs4T9+7sg9lb9h+908MWiKV8xtWciVTmnB3tivRjNerfXdxpfEBbq2NUvLMM5R9NLuyQg8nXT0PIh1xPd/wrcV49oJ6zbZaPlj2V87IY9T3F2XCOcW2MbZyZd49H+9m81E1N9SxlU+ff/1y+/f3719vf7788+Ugv/ffbMIH7ZNj0dsT4WMHHwLPu/Rp2O75uh99AK+N2xn7ZHq1OK6gczkN+9ngdOl1Qvki5xwSR8vFX6D+9vXA97B/+fr5rz9u/738uP328urP19vfP759e3n9Xs6jamvqlfJ/AAAA//+YAMZjDgkAAA=="
-
-type LoveCommand struct{}
-
-func (*LoveCommand) Name() string {
-	return "lovevictoria"
-}
-
-func (*LoveCommand) Hidden() bool {
-	return false
-}
-
-func (c *LoveCommand) Description() Description {
-	return Description{
-		Short: "",
-		Usage: []string{""},
-	}
-}
-
-func (*LoveCommand) Execute([]string) error {
-	c, err := base64.StdEncoding.DecodeString(content)
-	common.Must(err)
-	reader, err := gzip.NewReader(bytes.NewBuffer(c))
-	common.Must(err)
-	b := make([]byte, 4096)
-	nBytes, _ := reader.Read(b)
-
-	bb := bytes.NewBuffer(b[:nBytes])
-	scanner := bufio.NewScanner(bb)
-	for scanner.Scan() {
-		s := scanner.Text()
-		fmt.Print(s + platform.LineSeparator())
-	}
-
-	return nil
-}
-
-func init() {
-	common.Must(RegisterCommand(&LoveCommand{}))
-}

+ 4 - 45
infra/control/main/main.go

@@ -1,52 +1,11 @@
 package main
 
 import (
-	"flag"
-	"fmt"
-	"os"
-
-	"github.com/v2fly/v2ray-core/v4/common/log"
-	_ "github.com/v2fly/v2ray-core/v4/infra/conf/geodata/memconservative"
-	_ "github.com/v2fly/v2ray-core/v4/infra/conf/geodata/standard"
-	"github.com/v2fly/v2ray-core/v4/infra/control"
+	_ "github.com/v2fly/v2ray-core/v4/commands/all"
+	"github.com/v2fly/v2ray-core/v4/commands/base"
 )
 
-func getCommandName() string {
-	if len(os.Args) > 1 {
-		return os.Args[1]
-	}
-	return ""
-}
-
 func main() {
-	// let the v2ctl prints log at stderr
-	log.RegisterHandler(log.NewLogger(log.CreateStderrLogWriter()))
-	name := getCommandName()
-	cmd := control.GetCommand(name)
-	if cmd == nil {
-		fmt.Fprintln(os.Stderr, "Unknown command:", name)
-		fmt.Fprintln(os.Stderr)
-
-		fmt.Println("v2ctl <command>")
-		fmt.Println("Available commands:")
-		control.PrintUsage()
-		return
-	}
-
-	if err := cmd.Execute(os.Args[2:]); err != nil {
-		hasError := false
-		if err != flag.ErrHelp {
-			fmt.Fprintln(os.Stderr, err.Error())
-			fmt.Fprintln(os.Stderr)
-			hasError = true
-		}
-
-		for _, line := range cmd.Description().Usage {
-			fmt.Println(line)
-		}
-
-		if hasError {
-			os.Exit(-1)
-		}
-	}
+	base.RootCommand.Long = "A tool set for V2Ray."
+	base.Execute()
 }

+ 0 - 31
infra/control/uuid.go

@@ -1,31 +0,0 @@
-package control
-
-import (
-	"fmt"
-
-	"github.com/v2fly/v2ray-core/v4/common"
-	"github.com/v2fly/v2ray-core/v4/common/uuid"
-)
-
-type UUIDCommand struct{}
-
-func (c *UUIDCommand) Name() string {
-	return "uuid"
-}
-
-func (c *UUIDCommand) Description() Description {
-	return Description{
-		Short: "Generate new UUID",
-		Usage: []string{"v2ctl uuid"},
-	}
-}
-
-func (c *UUIDCommand) Execute([]string) error {
-	u := uuid.New()
-	fmt.Println(u.String())
-	return nil
-}
-
-func init() {
-	common.Must(RegisterCommand(&UUIDCommand{}))
-}

+ 0 - 64
infra/control/verify.go

@@ -1,64 +0,0 @@
-package control
-
-import (
-	"flag"
-	"os"
-
-	"github.com/v2fly/VSign/signerVerify"
-
-	"github.com/v2fly/v2ray-core/v4/common"
-)
-
-type VerifyCommand struct{}
-
-func (c *VerifyCommand) Name() string {
-	return "verify"
-}
-
-func (c *VerifyCommand) Description() Description {
-	return Description{
-		Short: "Verify if a binary is officially signed.",
-		Usage: []string{
-			"v2ctl verify --sig=<sig-file> file...",
-			"Verify the file officially signed by V2Ray.",
-		},
-	}
-}
-
-func (c *VerifyCommand) Execute(args []string) error {
-	fs := flag.NewFlagSet(c.Name(), flag.ContinueOnError)
-
-	sigFile := fs.String("sig", "", "Path to the signature file")
-
-	if err := fs.Parse(args); err != nil {
-		return err
-	}
-
-	target := fs.Arg(0)
-	if target == "" {
-		return newError("empty file path.")
-	}
-
-	if *sigFile == "" {
-		return newError("empty signature path.")
-	}
-
-	sigReader, err := os.Open(os.ExpandEnv(*sigFile))
-	if err != nil {
-		return newError("failed to open file ", *sigFile).Base(err)
-	}
-
-	files := fs.Args()
-
-	err = signerVerify.OutputAndJudge(signerVerify.CheckSignaturesV2Fly(sigReader, files))
-
-	if err == nil {
-		return nil
-	}
-
-	return newError("file is not officially signed by V2Ray").Base(err)
-}
-
-func init() {
-	common.Must(RegisterCommand(&VerifyCommand{}))
-}

+ 1 - 1
infra/conf/command/errors.generated.go → main/commands/errors.generated.go

@@ -1,4 +1,4 @@
-package command
+package commands
 
 import "github.com/v2fly/v2ray-core/v4/common/errors"
 

+ 169 - 0
main/commands/run.go

@@ -0,0 +1,169 @@
+package commands
+
+import (
+	"io/ioutil"
+	"log"
+	"os"
+	"os/signal"
+	"path"
+	"path/filepath"
+	"runtime"
+	"strings"
+	"syscall"
+
+	"github.com/v2fly/v2ray-core/v4"
+	"github.com/v2fly/v2ray-core/v4/commands/base"
+	"github.com/v2fly/v2ray-core/v4/common/cmdarg"
+	"github.com/v2fly/v2ray-core/v4/common/platform"
+)
+
+// CmdRun runs V2Ray with config
+var CmdRun = &base.Command{
+	CustomFlags: true,
+	UsageLine:   "{{.Exec}} run [-c config.json] [-confdir dir]",
+	Short:       "Run V2Ray with config",
+	Long: `
+Run V2Ray with config.
+
+Example:
+
+	{{.Exec}} {{.LongName}} -c config.json
+
+Arguments:
+
+	-c value
+		Short alias of -config
+
+	-config value
+		Config file for V2Ray. Multiple assign is accepted (only
+		json). Latter ones overrides the former ones.
+
+	-confdir string
+		A dir with multiple json config
+
+	-format string
+		Format of input files. (default "json")
+	`,
+}
+
+func init() {
+	CmdRun.Run = executeRun //break init loop
+}
+
+var (
+	configFiles  cmdarg.Arg // "Config file for V2Ray.", the option is customed type
+	configDir    string
+	configFormat *string
+)
+
+func setConfigFlags(cmd *base.Command) {
+	configFormat = cmd.Flag.String("format", "", "")
+
+	cmd.Flag.Var(&configFiles, "config", "")
+	cmd.Flag.Var(&configFiles, "c", "")
+	cmd.Flag.StringVar(&configDir, "confdir", "", "")
+}
+func executeRun(cmd *base.Command, args []string) {
+	setConfigFlags(cmd)
+	cmd.Flag.Parse(args)
+	printVersion()
+	server, err := startV2Ray()
+	if err != nil {
+		base.Fatalf("Failed to start: %s", err)
+	}
+
+	if err := server.Start(); err != nil {
+		base.Fatalf("Failed to start: %s", err)
+	}
+	defer server.Close()
+
+	// Explicitly triggering GC to remove garbage from config loading.
+	runtime.GC()
+
+	{
+		osSignals := make(chan os.Signal, 1)
+		signal.Notify(osSignals, os.Interrupt, syscall.SIGTERM)
+		<-osSignals
+	}
+}
+
+func fileExists(file string) bool {
+	info, err := os.Stat(file)
+	return err == nil && !info.IsDir()
+}
+
+func dirExists(file string) bool {
+	if file == "" {
+		return false
+	}
+	info, err := os.Stat(file)
+	return err == nil && info.IsDir()
+}
+
+func readConfDir(dirPath string) cmdarg.Arg {
+	confs, err := ioutil.ReadDir(dirPath)
+	if err != nil {
+		log.Fatalln(err)
+	}
+	files := make(cmdarg.Arg, 0)
+	for _, f := range confs {
+		if strings.HasSuffix(f.Name(), ".json") {
+			files.Set(path.Join(dirPath, f.Name()))
+		}
+	}
+	return files
+}
+
+func getConfigFilePath() cmdarg.Arg {
+	if dirExists(configDir) {
+		log.Println("Using confdir from arg:", configDir)
+		configFiles = append(configFiles, readConfDir(configDir)...)
+	} else if envConfDir := platform.GetConfDirPath(); dirExists(envConfDir) {
+		log.Println("Using confdir from env:", envConfDir)
+		configFiles = append(configFiles, readConfDir(envConfDir)...)
+	}
+	if len(configFiles) > 0 {
+		return configFiles
+	}
+
+	if workingDir, err := os.Getwd(); err == nil {
+		configFile := filepath.Join(workingDir, "config.json")
+		if fileExists(configFile) {
+			log.Println("Using default config: ", configFile)
+			return cmdarg.Arg{configFile}
+		}
+	}
+
+	if configFile := platform.GetConfigurationPath(); fileExists(configFile) {
+		log.Println("Using config from env: ", configFile)
+		return cmdarg.Arg{configFile}
+	}
+
+	log.Println("Using config from STDIN")
+	return cmdarg.Arg{"stdin:"}
+}
+
+func getFormatFromAlias() string {
+	switch strings.ToLower(*configFormat) {
+	case "pb":
+		return "protobuf"
+	default:
+		return *configFormat
+	}
+}
+
+func startV2Ray() (core.Server, error) {
+	configFiles := getConfigFilePath()
+
+	config, err := core.LoadConfig(getFormatFromAlias(), configFiles[0], configFiles)
+	if err != nil {
+		return nil, newError("failed to read config files: [", configFiles.String(), "]").Base(err)
+	}
+
+	server, err := core.New(config)
+	if err != nil {
+		return nil, newError("failed to create server").Base(err)
+	}
+
+	return server, nil
+}

+ 76 - 0
main/commands/test.go

@@ -0,0 +1,76 @@
+package commands
+
+import (
+	"fmt"
+	"log"
+
+	"github.com/v2fly/v2ray-core/v4"
+	"github.com/v2fly/v2ray-core/v4/commands/base"
+)
+
+// CmdTest tests config files
+var CmdTest = &base.Command{
+	CustomFlags: true,
+	UsageLine:   "{{.Exec}} test [-format=json] [-c config.json] [-confdir dir]",
+	Short:       "Test config files",
+	Long: `
+Test config files, without launching V2Ray server.
+
+Example:
+
+	{{.Exec}} {{.LongName}} -c config.json
+
+Arguments:
+
+	-c value
+		Short alias of -config
+
+	-config value
+		Config file for V2Ray. Multiple assign is accepted (only
+		json). Latter ones overrides the former ones.
+
+	-confdir string
+		A dir with multiple json config
+
+	-format string
+		Format of input files. (default "json")
+	`,
+}
+
+func init() {
+	CmdTest.Run = executeTest //break init loop
+}
+
+func executeTest(cmd *base.Command, args []string) {
+	setConfigFlags(cmd)
+	cmd.Flag.Parse(args)
+	if dirExists(configDir) {
+		log.Println("Using confdir from arg:", configDir)
+		configFiles = append(configFiles, readConfDir(configDir)...)
+	}
+	if len(configFiles) == 0 {
+		cmd.Flag.Usage()
+		base.SetExitStatus(1)
+		base.Exit()
+	}
+	printVersion()
+	_, err := startV2RayTesting()
+	if err != nil {
+		base.Fatalf("Test failed: %s", err)
+	}
+	fmt.Println("Configuration OK.")
+}
+
+func startV2RayTesting() (core.Server, error) {
+	config, err := core.LoadConfig(getFormatFromAlias(), configFiles[0], configFiles)
+	if err != nil {
+		return nil, newError("failed to read config files: [", configFiles.String(), "]").Base(err)
+	}
+
+	server, err := core.New(config)
+	if err != nil {
+		return nil, newError("failed to create server").Base(err)
+	}
+
+	return server, nil
+}

+ 28 - 0
main/commands/version.go

@@ -0,0 +1,28 @@
+package commands
+
+import (
+	"fmt"
+
+	"github.com/v2fly/v2ray-core/v4"
+	"github.com/v2fly/v2ray-core/v4/commands/base"
+)
+
+// CmdVersion prints V2Ray Versions
+var CmdVersion = &base.Command{
+	UsageLine: "{{.Exec}} version",
+	Short:     "Print V2Ray Versions",
+	Long: `Version prints the build information for V2Ray executables.
+`,
+	Run: executeVersion,
+}
+
+func executeVersion(cmd *base.Command, args []string) {
+	printVersion()
+}
+
+func printVersion() {
+	version := core.VersionStatement()
+	for _, s := range version {
+		fmt.Println(s)
+	}
+}

+ 1 - 1
main/confloader/external/external.go

@@ -73,7 +73,7 @@ func FetchHTTPContent(target string) ([]byte, error) {
 }
 
 func ExtConfigLoader(files []string, reader io.Reader) (io.Reader, error) {
-	buf, err := ctlcmd.Run(append([]string{"config"}, files...), reader)
+	buf, err := ctlcmd.Run(append([]string{"convert"}, files...), reader)
 	if err != nil {
 		return nil, err
 	}

+ 3 - 0
main/distro/all/all.go

@@ -79,4 +79,7 @@ import (
 
 	// Load config from file or http(s)
 	_ "github.com/v2fly/v2ray-core/v4/main/confloader/external"
+
+	// commands
+	_ "github.com/v2fly/v2ray-core/v4/commands/all"
 )

+ 20 - 156
main/main.go

@@ -1,165 +1,29 @@
 package main
 
-//go:generate go run github.com/v2fly/v2ray-core/v4/common/errors/errorgen
-
 import (
-	"flag"
-	"fmt"
-	"io/ioutil"
-	"log"
-	"os"
-	"os/signal"
-	"path"
-	"path/filepath"
-	"runtime"
-	"strings"
-	"syscall"
-
-	core "github.com/v2fly/v2ray-core/v4"
-	"github.com/v2fly/v2ray-core/v4/common/cmdarg"
-	"github.com/v2fly/v2ray-core/v4/common/platform"
+	"github.com/v2fly/v2ray-core/v4/commands/base"
+	"github.com/v2fly/v2ray-core/v4/main/commands"
 	_ "github.com/v2fly/v2ray-core/v4/main/distro/all"
 )
 
-var (
-	configFiles cmdarg.Arg // "Config file for V2Ray.", the option is customed type, parse in main
-	configDir   string
-	version     = flag.Bool("version", false, "Show current version of V2Ray.")
-	test        = flag.Bool("test", false, "Test config file only, without launching V2Ray server.")
-	format      = flag.String("format", "json", "Format of input file.")
-
-	/* We have to do this here because Golang's Test will also need to parse flag, before
-	 * main func in this file is run.
-	 */
-	_ = func() error { // nolint: unparam
-		flag.Var(&configFiles, "config", "Config file for V2Ray. Multiple assign is accepted (only json). Latter ones overrides the former ones.")
-		flag.Var(&configFiles, "c", "Short alias of -config")
-		flag.StringVar(&configDir, "confdir", "", "A dir with multiple json config")
-
-		return nil
-	}()
-)
-
-func fileExists(file string) bool {
-	info, err := os.Stat(file)
-	return err == nil && !info.IsDir()
-}
-
-func dirExists(file string) bool {
-	if file == "" {
-		return false
-	}
-	info, err := os.Stat(file)
-	return err == nil && info.IsDir()
-}
-
-func readConfDir(dirPath string) {
-	confs, err := ioutil.ReadDir(dirPath)
-	if err != nil {
-		log.Fatalln(err)
-	}
-	for _, f := range confs {
-		if strings.HasSuffix(f.Name(), ".json") {
-			configFiles.Set(path.Join(dirPath, f.Name()))
-		}
-	}
-}
-
-func getConfigFilePath() cmdarg.Arg {
-	if dirExists(configDir) {
-		log.Println("Using confdir from arg:", configDir)
-		readConfDir(configDir)
-	} else if envConfDir := platform.GetConfDirPath(); dirExists(envConfDir) {
-		log.Println("Using confdir from env:", envConfDir)
-		readConfDir(envConfDir)
-	}
-
-	if len(configFiles) > 0 {
-		return configFiles
-	}
-
-	if workingDir, err := os.Getwd(); err == nil {
-		configFile := filepath.Join(workingDir, "config.json")
-		if fileExists(configFile) {
-			log.Println("Using default config: ", configFile)
-			return cmdarg.Arg{configFile}
-		}
-	}
-
-	if configFile := platform.GetConfigurationPath(); fileExists(configFile) {
-		log.Println("Using config from env: ", configFile)
-		return cmdarg.Arg{configFile}
-	}
-
-	log.Println("Using config from STDIN")
-	return cmdarg.Arg{"stdin:"}
-}
-
-func GetConfigFormat() string {
-	switch strings.ToLower(*format) {
-	case "pb", "protobuf":
-		return "protobuf"
-	default:
-		return "json"
-	}
-}
-
-func startV2Ray() (core.Server, error) {
-	configFiles := getConfigFilePath()
-
-	config, err := core.LoadConfig(GetConfigFormat(), configFiles[0], configFiles)
-	if err != nil {
-		return nil, newError("failed to read config files: [", configFiles.String(), "]").Base(err)
-	}
-
-	server, err := core.New(config)
-	if err != nil {
-		return nil, newError("failed to create server").Base(err)
-	}
-
-	return server, nil
-}
-
-func printVersion() {
-	version := core.VersionStatement()
-	for _, s := range version {
-		fmt.Println(s)
-	}
-}
-
 func main() {
-	flag.Parse()
-
-	printVersion()
-
-	if *version {
-		return
-	}
-
-	server, err := startV2Ray()
-	if err != nil {
-		fmt.Println(err)
-		// Configuration error. Exit with a special value to prevent systemd from restarting.
-		os.Exit(23)
-	}
-
-	if *test {
-		fmt.Println("Configuration OK.")
-		os.Exit(0)
-	}
-
-	if err := server.Start(); err != nil {
-		fmt.Println("Failed to start", err)
-		os.Exit(-1)
-	}
-	defer server.Close()
-
-	// Explicitly triggering GC to remove garbage from config loading.
-	runtime.GC()
-
-	{
-		osSignals := make(chan os.Signal, 1)
-		signal.Notify(osSignals, os.Interrupt, syscall.SIGTERM)
-		<-osSignals
+	base.RootCommand.Long = "A unified platform for anti-censorship."
+	base.RegisterCommand(commands.CmdRun)
+	base.RegisterCommand(commands.CmdVersion)
+	base.RegisterCommand(commands.CmdTest)
+	base.SortLessFunc = runIsTheFirst
+	base.SortCommands()
+	base.Execute()
+}
+
+func runIsTheFirst(i, j *base.Command) bool {
+	left := i.Name()
+	right := j.Name()
+	if left == "run" {
+		return true
+	}
+	if right == "run" {
+		return false
 	}
+	return left < right
 }