diff --git a/infra/control/api.go b/commands/all/api.go similarity index 60% rename from infra/control/api.go rename to commands/all/api.go index 5d435b006..ad36e76cf 100644 --- a/infra/control/api.go +++ b/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] ", + 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: + + 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 (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 ''", - }, - } +func init() { + cmdAPI.Run = executeAPI // break init loop } -func (c *APICommand) Execute(args []string) error { - fs := flag.NewFlagSet(c.Name(), flag.ContinueOnError) +var ( + apiServerAddrPtr = cmdAPI.Flag.String("server", "127.0.0.1:8080", "") +) - serverAddrPtr := fs.String("server", "127.0.0.1:8080", "Server address") - - if err := fs.Parse(args); err != nil { - return err - } - - unnamedArgs := fs.Args() +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{})) -} diff --git a/infra/control/certchainhash.go b/commands/all/certchainhash.go similarity index 74% rename from infra/control/certchainhash.go rename to commands/all/certchainhash.go index 1726ecc28..77939755f 100644 --- a/infra/control/certchainhash.go +++ b/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 ", - "Calculate TLS certificate chain hash.", - }, +func (c CertificateChainHashCommand) Description() base.Command { + return base.Command{ + Short: "Calculate TLS certificates hash.", + UsageLine: "v2ctl certChainHash --cert .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 +} diff --git a/infra/control/errors.generated.go b/commands/all/errors.generated.go similarity index 92% rename from infra/control/errors.generated.go rename to commands/all/errors.generated.go index 5d8e82f22..4131a75f9 100644 --- a/infra/control/errors.generated.go +++ b/commands/all/errors.generated.go @@ -1,4 +1,4 @@ -package control +package all import "github.com/v2fly/v2ray-core/v4/common/errors" diff --git a/commands/all/love.go b/commands/all/love.go new file mode 100644 index 000000000..a7d24b9e3 --- /dev/null +++ b/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()) + } +} diff --git a/commands/all/tls.go b/commands/all/tls.go new file mode 100644 index 000000000..627efec88 --- /dev/null +++ b/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, + }, +} diff --git a/commands/all/tlscmd/cert.go b/commands/all/tlscmd/cert.go new file mode 100644 index 000000000..53d5e01b0 --- /dev/null +++ b/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"` +} diff --git a/infra/control/tlsping.go b/commands/all/tlscmd/ping.go similarity index 70% rename from infra/control/tlsping.go rename to commands/all/tlscmd/ping.go index 2231b1041..cc0860226 100644 --- a/infra/control/tlsping.go +++ b/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 ] ", + Short: "Ping the domain with TLS handshake", + Long: ` +Ping the domain with TLS handshake. -func (c *TLSPingCommand) Name() string { - return "tlsping" +Arguments: + + -ip + The IP address of the domain. +`, } -func (c *TLSPingCommand) Description() Description { - return Description{ - Short: "Ping the domain with TLS handshake", - Usage: []string{"v2ctl tlsping --ip "}, - } +func init() { + CmdPing.Run = executePing // break init loop } -func printCertificates(certs []*x509.Certificate) { - for _, cert := range certs { - if len(cert.DNSNames) == 0 { - continue - } - fmt.Println("Allowed domains: ", cert.DNSNames) - } -} +var ( + pingIPStr = CmdPing.Flag.String("ip", "", "") +) -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) +func executePing(cmd *base.Command, args []string) { + if CmdPing.Flag.NArg() < 1 { + base.Fatalf("domain not specified") } - if fs.NArg() < 1 { - return newError("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{})) -} diff --git a/commands/all/uuid.go b/commands/all/uuid.go new file mode 100644 index 000000000..2caaf0e79 --- /dev/null +++ b/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()) +} diff --git a/commands/all/verify.go b/commands/all/verify.go new file mode 100644 index 000000000..696f797df --- /dev/null +++ b/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) + } +} diff --git a/commands/base/command.go b/commands/base/command.go new file mode 100644 index 000000000..6fa606fda --- /dev/null +++ b/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 ' 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 +} diff --git a/commands/base/env.go b/commands/base/env.go new file mode 100644 index 000000000..d75eaa546 --- /dev/null +++ b/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) +} diff --git a/commands/base/execute.go b/commands/base/execute.go new file mode 100644 index 000000000..da73a4829 --- /dev/null +++ b/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() +} diff --git a/commands/base/help.go b/commands/base/help.go new file mode 100644 index 000000000..8dd534c32 --- /dev/null +++ b/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}} [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}} " 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}} " 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, + } +} diff --git a/commands/base/root.go b/commands/base/root.go new file mode 100644 index 000000000..8f3bf82b4 --- /dev/null +++ b/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) +} diff --git a/config.go b/config.go index 2d4db715b..65439a13f 100644 --- a/config.go +++ b/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() diff --git a/infra/conf/command/command.go b/infra/conf/command/command.go deleted file mode 100644 index 7b4890627..000000000 --- a/infra/conf/command/command.go +++ /dev/null @@ -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{})) -} diff --git a/infra/control/cert.go b/infra/control/cert.go deleted file mode 100644 index 9e0308106..000000000 --- a/infra/control/cert.go +++ /dev/null @@ -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{})) -} diff --git a/infra/control/command.go b/infra/control/command.go deleted file mode 100644 index 1edeac10c..000000000 --- a/infra/control/command.go +++ /dev/null @@ -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()) - } -} diff --git a/infra/control/config.go b/infra/control/config.go deleted file mode 100644 index 6a1f9b50d..000000000 --- a/infra/control/config.go +++ /dev/null @@ -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 .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{})) -} diff --git a/infra/control/control.go b/infra/control/control.go deleted file mode 100644 index fb604991b..000000000 --- a/infra/control/control.go +++ /dev/null @@ -1,3 +0,0 @@ -package control - -//go:generate go run github.com/v2fly/v2ray-core/v4/common/errors/errorgen diff --git a/infra/control/fetch.go b/infra/control/fetch.go deleted file mode 100644 index aee054bc5..000000000 --- a/infra/control/fetch.go +++ /dev/null @@ -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 "}, - } -} - -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{})) -} diff --git a/infra/control/love.go b/infra/control/love.go deleted file mode 100644 index 6dfff967f..000000000 --- a/infra/control/love.go +++ /dev/null @@ -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{})) -} diff --git a/infra/control/main/main.go b/infra/control/main/main.go index f189dccf0..3b7f4a4ed 100644 --- a/infra/control/main/main.go +++ b/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 ") - 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() } diff --git a/infra/control/uuid.go b/infra/control/uuid.go deleted file mode 100644 index 5a8273d84..000000000 --- a/infra/control/uuid.go +++ /dev/null @@ -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{})) -} diff --git a/infra/control/verify.go b/infra/control/verify.go deleted file mode 100644 index 48bed7f32..000000000 --- a/infra/control/verify.go +++ /dev/null @@ -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= 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{})) -} diff --git a/infra/conf/command/errors.generated.go b/main/commands/errors.generated.go similarity index 92% rename from infra/conf/command/errors.generated.go rename to main/commands/errors.generated.go index 3b1f040e5..4c4f268ed 100644 --- a/infra/conf/command/errors.generated.go +++ b/main/commands/errors.generated.go @@ -1,4 +1,4 @@ -package command +package commands import "github.com/v2fly/v2ray-core/v4/common/errors" diff --git a/main/commands/run.go b/main/commands/run.go new file mode 100644 index 000000000..5145a1ab7 --- /dev/null +++ b/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 +} diff --git a/main/commands/test.go b/main/commands/test.go new file mode 100644 index 000000000..206801302 --- /dev/null +++ b/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 +} diff --git a/main/commands/version.go b/main/commands/version.go new file mode 100644 index 000000000..df7bbcf9f --- /dev/null +++ b/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) + } +} diff --git a/main/confloader/external/external.go b/main/confloader/external/external.go index 7e543b1e2..138ec66b3 100644 --- a/main/confloader/external/external.go +++ b/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 } diff --git a/main/distro/all/all.go b/main/distro/all/all.go index 2a653910e..117c43eda 100644 --- a/main/distro/all/all.go +++ b/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" ) diff --git a/main/main.go b/main/main.go index c2ff3d3fc..df5f37f3a 100644 --- a/main/main.go +++ b/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 main() { + 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 dirExists(file string) bool { - if file == "" { +func runIsTheFirst(i, j *base.Command) bool { + left := i.Name() + right := j.Name() + if left == "run" { + return true + } + if right == "run" { 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 - } + return left < right }