1
0
mirror of https://github.com/v2fly/v2ray-core.git synced 2024-11-12 07:19:01 -05:00

go style commands, merge v2ctl commands, v5 oriented (#369)

* go style commands
merge v2ctl commandsw

* migrate go style commands to v2ctl

* fixes & code optimize

* sort the commands

* update commands description

* restore old proto
golang.org proto has removed UnmarshalText, without alternative

* add test command

* remove unused code

* code optimize and fix
* The commit simplifies the run and test commands code,
* Fixes a hidden issue that the format flag not applied in command "v2ray test -format=pb ..."

* fix default loader logic
This commit is contained in:
Jebbs 2020-11-23 23:38:43 +08:00 committed by GitHub
parent bb74ef99e2
commit 521120d196
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
33 changed files with 1206 additions and 853 deletions

View File

@ -1,9 +1,8 @@
package control package all
import ( import (
"context" "context"
"errors" "errors"
"flag"
"fmt" "fmt"
"strings" "strings"
"time" "time"
@ -13,71 +12,66 @@ import (
logService "v2ray.com/core/app/log/command" logService "v2ray.com/core/app/log/command"
statsService "v2ray.com/core/app/stats/command" statsService "v2ray.com/core/app/stats/command"
"v2ray.com/core/common" "v2ray.com/core/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 { The following methods are currently supported:
return "api"
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 { func init() {
return Description{ cmdAPI.Run = executeAPI // break init loop
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 (c *APICommand) Execute(args []string) error { var (
fs := flag.NewFlagSet(c.Name(), flag.ContinueOnError) apiServerAddrPtr = cmdAPI.Flag.String("server", "127.0.0.1:8080", "")
)
serverAddrPtr := fs.String("server", "127.0.0.1:8080", "Server address") func executeAPI(cmd *base.Command, args []string) {
unnamedArgs := cmdAPI.Flag.Args()
if err := fs.Parse(args); err != nil {
return err
}
unnamedArgs := fs.Args()
if len(unnamedArgs) < 2 { 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]) service, method := getServiceMethod(unnamedArgs[0])
handler, found := serivceHandlerMap[strings.ToLower(service)] handler, found := serivceHandlerMap[strings.ToLower(service)]
if !found { if !found {
return newError("unknown service: ", service) base.Fatalf("unknown service: %s", service)
} }
ctx, cancel := context.WithTimeout(context.Background(), 3*time.Second) ctx, cancel := context.WithTimeout(context.Background(), 3*time.Second)
defer cancel() 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 { if err != nil {
return newError("failed to dial ", *serverAddrPtr).Base(err) base.Fatalf("failed to dial %s", *apiServerAddrPtr)
} }
defer conn.Close() defer conn.Close()
response, err := handler(ctx, conn, method, unnamedArgs[1]) response, err := handler(ctx, conn, method, unnamedArgs[1])
if err != nil { 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) fmt.Println(response)
return nil
} }
func getServiceMethod(s string) (string, string) { 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) { switch strings.ToLower(method) {
case "restartlogger": case "restartlogger":
r := &logService.RestartLoggerRequest{} r := &logService.RestartLoggerRequest{}
if err := proto.UnmarshalText(request, r); err != nil {
return "", err
}
resp, err := client.RestartLogger(ctx, r) resp, err := client.RestartLogger(ctx, r)
if err != nil { if err != nil {
return "", err return "", err
@ -152,7 +143,3 @@ func callStatsService(ctx context.Context, conn *grpc.ClientConn, method string,
return "", errors.New("Unknown method: " + method) return "", errors.New("Unknown method: " + method)
} }
} }
func init() {
common.Must(RegisterCommand(&APICommand{}))
}

17
commands/all/commands.go Normal file
View File

@ -0,0 +1,17 @@
package all
import "v2ray.com/core/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
commands/all/convert.go Normal file
View File

@ -0,0 +1,126 @@
package all
import (
"bytes"
"fmt"
"io"
"io/ioutil"
"net/http"
"net/url"
"os"
"strings"
"time"
"google.golang.org/protobuf/proto"
"v2ray.com/core/commands/base"
"v2ray.com/core/common"
"v2ray.com/core/common/buf"
"v2ray.com/core/infra/conf"
"v2ray.com/core/infra/conf/serial"
)
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
}

View File

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

37
commands/all/love.go Normal file
View File

@ -0,0 +1,37 @@
package all
import (
"bufio"
"bytes"
"compress/gzip"
"encoding/base64"
"fmt"
"v2ray.com/core/commands/base"
"v2ray.com/core/common"
"v2ray.com/core/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
commands/all/tls.go Normal file
View File

@ -0,0 +1,18 @@
package all
import (
"v2ray.com/core/commands/all/tlscmd"
"v2ray.com/core/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
commands/all/tlscmd/cert.go Normal file
View File

@ -0,0 +1,143 @@
package tlscmd
import (
"context"
"crypto/x509"
"encoding/json"
"os"
"strings"
"time"
"v2ray.com/core/commands/base"
"v2ray.com/core/common"
"v2ray.com/core/common/protocol/tls/cert"
"v2ray.com/core/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"`
}

View File

@ -1,63 +1,55 @@
package control package tlscmd
import ( import (
"crypto/tls" "crypto/tls"
"crypto/x509" "crypto/x509"
"flag"
"fmt" "fmt"
"net" "net"
"v2ray.com/core/common" "v2ray.com/core/commands/base"
) )
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 { Arguments:
return "tlsping"
-ip
The IP address of the domain.
`,
} }
func (c *TLSPingCommand) Description() Description { func init() {
return Description{ CmdPing.Run = executePing // break init loop
Short: "Ping the domain with TLS handshake",
Usage: []string{"v2ctl tlsping <domain> --ip <ip>"},
}
} }
func printCertificates(certs []*x509.Certificate) { var (
for _, cert := range certs { pingIPStr = CmdPing.Flag.String("ip", "", "")
if len(cert.DNSNames) == 0 { )
continue
}
fmt.Println("Allowed domains: ", cert.DNSNames)
}
}
func (c *TLSPingCommand) Execute(args []string) error { func executePing(cmd *base.Command, args []string) {
fs := flag.NewFlagSet(c.Name(), flag.ContinueOnError) if CmdPing.Flag.NArg() < 1 {
ipStr := fs.String("ip", "", "IP address of the domain") base.Fatalf("domain not specified")
if err := fs.Parse(args); err != nil {
return newError("flag parsing").Base(err)
} }
if fs.NArg() < 1 { domain := CmdPing.Flag.Arg(0)
return newError("domain not specified")
}
domain := fs.Arg(0)
fmt.Println("Tls ping: ", domain) fmt.Println("Tls ping: ", domain)
var ip net.IP var ip net.IP
if len(*ipStr) > 0 { if len(*pingIPStr) > 0 {
v := net.ParseIP(*ipStr) v := net.ParseIP(*pingIPStr)
if v == nil { if v == nil {
return newError("invalid IP: ", *ipStr) base.Fatalf("invalid IP: %s", *pingIPStr)
} }
ip = v ip = v
} else { } else {
v, err := net.ResolveIPAddr("ip", domain) v, err := net.ResolveIPAddr("ip", domain)
if err != nil { if err != nil {
return newError("resolve IP").Base(err) base.Fatalf("Failed to resolve IP: %s", err)
} }
ip = v.IP ip = v.IP
} }
@ -68,7 +60,7 @@ func (c *TLSPingCommand) Execute(args []string) error {
{ {
tcpConn, err := net.DialTCP("tcp", nil, &net.TCPAddr{IP: ip, Port: 443}) tcpConn, err := net.DialTCP("tcp", nil, &net.TCPAddr{IP: ip, Port: 443})
if err != nil { if err != nil {
return newError("dial tcp").Base(err) base.Fatalf("Failed to dial tcp: %s", err)
} }
tlsConn := tls.Client(tcpConn, &tls.Config{ tlsConn := tls.Client(tcpConn, &tls.Config{
InsecureSkipVerify: true, InsecureSkipVerify: true,
@ -91,7 +83,7 @@ func (c *TLSPingCommand) Execute(args []string) error {
{ {
tcpConn, err := net.DialTCP("tcp", nil, &net.TCPAddr{IP: ip, Port: 443}) tcpConn, err := net.DialTCP("tcp", nil, &net.TCPAddr{IP: ip, Port: 443})
if err != nil { if err != nil {
return newError("dial tcp").Base(err) base.Fatalf("Failed to dial tcp: %s", err)
} }
tlsConn := tls.Client(tcpConn, &tls.Config{ tlsConn := tls.Client(tcpConn, &tls.Config{
ServerName: domain, ServerName: domain,
@ -110,10 +102,13 @@ func (c *TLSPingCommand) Execute(args []string) error {
} }
fmt.Println("Tls ping finished") fmt.Println("Tls ping finished")
return nil
} }
func init() { func printCertificates(certs []*x509.Certificate) {
common.Must(RegisterCommand(&TLSPingCommand{})) for _, cert := range certs {
if len(cert.DNSNames) == 0 {
continue
}
fmt.Println("Allowed domains: ", cert.DNSNames)
}
} }

21
commands/all/uuid.go Normal file
View File

@ -0,0 +1,21 @@
package all
import (
"fmt"
"v2ray.com/core/commands/base"
"v2ray.com/core/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
commands/all/verify.go Normal file
View File

@ -0,0 +1,53 @@
package all
import (
"os"
"github.com/v2fly/VSign/signerVerify"
"v2ray.com/core/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
commands/base/command.go Normal file
View File

@ -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
commands/base/env.go Normal file
View File

@ -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
commands/base/execute.go Normal file
View File

@ -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
commands/base/help.go Normal file
View File

@ -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
commands/base/root.go Normal file
View File

@ -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)
}

View File

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

View File

@ -1,48 +0,0 @@
package command
//go:generate go run v2ray.com/core/common/errors/errorgen
import (
"os"
"github.com/golang/protobuf/proto"
"v2ray.com/core/common"
"v2ray.com/core/infra/conf/serial"
"v2ray.com/core/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{}))
}

View File

@ -1,139 +0,0 @@
package control
import (
"context"
"crypto/x509"
"encoding/json"
"flag"
"os"
"strings"
"time"
"v2ray.com/core/common"
"v2ray.com/core/common/protocol/tls/cert"
"v2ray.com/core/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=v2ray.com] [--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{}))
}

View File

@ -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())
}
}

View File

@ -1,90 +0,0 @@
package control
import (
"bytes"
"io"
"io/ioutil"
"os"
"strings"
"github.com/golang/protobuf/proto"
"v2ray.com/core/common"
"v2ray.com/core/infra/conf"
"v2ray.com/core/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{}))
}

View File

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

View File

@ -1,78 +0,0 @@
package control
import (
"net/http"
"net/url"
"os"
"strings"
"time"
"v2ray.com/core/common"
"v2ray.com/core/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{}))
}

View File

@ -1,53 +0,0 @@
package control
import (
"bufio"
"bytes"
"compress/gzip"
"encoding/base64"
"fmt"
"v2ray.com/core/common"
"v2ray.com/core/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{}))
}

View File

@ -1,51 +1,11 @@
package main package main
import ( import (
"flag" _ "v2ray.com/core/commands/all"
"fmt" "v2ray.com/core/commands/base"
"os"
commlog "v2ray.com/core/common/log"
// _ "v2ray.com/core/infra/conf/command"
"v2ray.com/core/infra/control"
) )
func getCommandName() string {
if len(os.Args) > 1 {
return os.Args[1]
}
return ""
}
func main() { func main() {
// let the v2ctl prints log at stderr base.RootCommand.Long = "A tool set for V2Ray."
commlog.RegisterHandler(commlog.NewLogger(commlog.CreateStderrLogWriter())) base.Execute()
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)
}
}
} }

View File

@ -1,31 +0,0 @@
package control
import (
"fmt"
"v2ray.com/core/common"
"v2ray.com/core/common/uuid"
)
type UUIDCommand struct{}
func (c *UUIDCommand) Name() string {
return "uuid"
}
func (c *UUIDCommand) Description() Description {
return Description{
Short: "Generate new UUIDs",
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{}))
}

View File

@ -1,63 +0,0 @@
package control
import (
"flag"
"os"
"github.com/v2fly/VSign/signerVerify"
"v2ray.com/core/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{}))
}

View File

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

169
main/commands/run.go Normal file
View File

@ -0,0 +1,169 @@
package commands
import (
"io/ioutil"
"log"
"os"
"os/signal"
"path"
"path/filepath"
"runtime"
"strings"
"syscall"
"v2ray.com/core"
"v2ray.com/core/commands/base"
"v2ray.com/core/common/cmdarg"
"v2ray.com/core/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
main/commands/test.go Normal file
View File

@ -0,0 +1,76 @@
package commands
import (
"fmt"
"log"
"v2ray.com/core"
"v2ray.com/core/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
main/commands/version.go Normal file
View File

@ -0,0 +1,28 @@
package commands
import (
"fmt"
"v2ray.com/core"
"v2ray.com/core/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)
}
}

View File

@ -73,7 +73,7 @@ func FetchHTTPContent(target string) ([]byte, error) {
} }
func ExtConfigLoader(files []string, reader io.Reader) (io.Reader, 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 { if err != nil {
return nil, err return nil, err
} }

View File

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

View File

@ -1,165 +1,29 @@
package main package main
//go:generate go run v2ray.com/core/common/errors/errorgen
import ( import (
"flag" "v2ray.com/core/commands/base"
"fmt" "v2ray.com/core/main/commands"
"io/ioutil"
"log"
"os"
"os/signal"
"path"
"path/filepath"
"runtime"
"strings"
"syscall"
"v2ray.com/core"
"v2ray.com/core/common/cmdarg"
"v2ray.com/core/common/platform"
_ "v2ray.com/core/main/distro/all" _ "v2ray.com/core/main/distro/all"
) )
var ( func main() {
configFiles cmdarg.Arg // "Config file for V2Ray.", the option is customed type, parse in main base.RootCommand.Long = "A unified platform for anti-censorship."
configDir string base.RegisterCommand(commands.CmdRun)
version = flag.Bool("version", false, "Show current version of V2Ray.") base.RegisterCommand(commands.CmdVersion)
test = flag.Bool("test", false, "Test config file only, without launching V2Ray server.") base.RegisterCommand(commands.CmdTest)
format = flag.String("format", "json", "Format of input file.") base.SortLessFunc = runIsTheFirst
base.SortCommands()
/* We have to do this here because Golang's Test will also need to parse flag, before base.Execute()
* 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 { func runIsTheFirst(i, j *base.Command) bool {
if file == "" { left := i.Name()
right := j.Name()
if left == "run" {
return true
}
if right == "run" {
return false return false
} }
info, err := os.Stat(file) return left < right
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
}
} }