diff --git a/README.md b/README.md index 49c5f80..87a82e7 100644 --- a/README.md +++ b/README.md @@ -90,6 +90,10 @@ cd autocar go build -trimpath -o autocar ./cmd/autocar ``` +源码构建的各选项型命令只接受命名参数,不接受额外的位置参数(包括 `--` 后的内容)。 +`token`、`cert`、`init` 遇到这类输入会先报错,不生成凭据或配置文件; +正常参数、`-h/--help` 与既有配置文件覆盖规则保持不变。 + ### 新部署:生成两端配置并先检查 以下 `init` 与 `--check` 需要包含这些功能的源码构建,已发布的 v1.0.1 不支持。 diff --git a/cmd/autocar/bench.go b/cmd/autocar/bench.go index 3dc303d..3adb52e 100644 --- a/cmd/autocar/bench.go +++ b/cmd/autocar/bench.go @@ -22,7 +22,7 @@ func runBenchServer(ctx context.Context, args []string) error { allowPublic := fs.Bool("allow-public-benchmark", false, "allow an unauthenticated benchmark listener outside loopback") maxBytes := fs.Int64("max-bytes", 64<<20, "maximum bytes per transfer") maxConnections := fs.Int("max-connections", 16, "maximum concurrent transfers") - if err := fs.Parse(args); err != nil { + if err := parseCommandFlags(fs, args); err != nil { return err } if *maxBytes <= 0 || *maxConnections <= 0 { @@ -100,7 +100,7 @@ func runBenchClient(parent context.Context, args []string) error { warmup := fs.Int("warmup", 1, "unmeasured warmup iterations") timeout := fs.Duration("timeout", 2*time.Minute, "timeout per transfer") jsonOutput := fs.Bool("json", false, "emit machine-readable JSON") - if err := fs.Parse(args); err != nil { + if err := parseCommandFlags(fs, args); err != nil { return err } if *target == "" { diff --git a/cmd/autocar/cert.go b/cmd/autocar/cert.go index 1f51390..35e50d5 100644 --- a/cmd/autocar/cert.go +++ b/cmd/autocar/cert.go @@ -17,7 +17,7 @@ func runCert(args []string) error { keyFile := fs.String("key", "server.key", "0600 private-key output path") days := fs.Int("days", 365, "certificate validity in days (1-1825)") organization := fs.String("organization", "AutoCAR", "certificate organization") - if err := fs.Parse(args); err != nil { + if err := parseCommandFlags(fs, args); err != nil { return err } if strings.TrimSpace(*hosts) == "" { diff --git a/cmd/autocar/cli_arguments.go b/cmd/autocar/cli_arguments.go new file mode 100644 index 0000000..535e31b --- /dev/null +++ b/cmd/autocar/cli_arguments.go @@ -0,0 +1,19 @@ +package main + +import ( + "flag" + "fmt" +) + +// None of the option-based commands accepts positional operands. Check them +// before reading credentials, creating output files, or starting network I/O. +// Do not echo the operands: an accidentally pasted value may be a secret. +func parseCommandFlags(fs *flag.FlagSet, args []string) error { + if err := fs.Parse(args); err != nil { + return err + } + if fs.NArg() != 0 { + return fmt.Errorf("%s does not accept positional arguments; use named options", fs.Name()) + } + return nil +} diff --git a/cmd/autocar/cli_arguments_test.go b/cmd/autocar/cli_arguments_test.go new file mode 100644 index 0000000..6362eca --- /dev/null +++ b/cmd/autocar/cli_arguments_test.go @@ -0,0 +1,65 @@ +package main + +import ( + "context" + "os" + "path/filepath" + "strings" + "testing" +) + +func TestCredentialCommandsRejectPositionalArgumentsBeforeWriting(t *testing.T) { + const unexpected = "private-unexpected-argument" + for _, command := range []string{"token", "cert", "init"} { + for _, suffix := range [][]string{ + {unexpected}, {"--", unexpected}, {unexpected, "--unknown-option"}, + } { + t.Run(command+"/"+strings.Join(suffix, " "), func(t *testing.T) { + directory := t.TempDir() + var args, outputs []string + switch command { + case "token": + outputs = []string{filepath.Join(directory, "token")} + args = []string{"token", "--out", outputs[0]} + case "cert": + outputs = []string{filepath.Join(directory, "cert.pem"), filepath.Join(directory, "key.pem")} + args = []string{"cert", "--hosts", "relay.invalid", "--cert", outputs[0], "--key", outputs[1]} + case "init": + outputs = []string{filepath.Join(directory, "bundle")} + args = []string{"init", "--server", "relay.invalid:443", "--out", outputs[0]} + } + err := run(context.Background(), append(args, suffix...)) + if err == nil || !strings.Contains(err.Error(), "positional arguments") { + t.Errorf("want positional argument rejection, got %v", err) + } + if err != nil && strings.Contains(err.Error(), unexpected) { + t.Error("error disclosed the unexpected argument value") + } + for _, path := range outputs { + if _, err := os.Lstat(path); !os.IsNotExist(err) { + t.Errorf("output %s was created or could not be checked: %v", filepath.Base(path), err) + } + } + }) + } + } +} + +func TestBenchmarkCommandsRejectPositionalArgumentsBeforeStartup(t *testing.T) { + // Invalid required settings make the unfixed baseline return before any + // listener or network connection. Argument rejection must precede those + // semantic checks, not be inferred from a downstream startup failure. + for _, args := range [][]string{ + {"bench-server", "--listen", "invalid-listener", "unexpected"}, + {"bench-server", "--listen", "invalid-listener", "--", "unexpected"}, + {"bench-client", "--target=", "unexpected"}, + {"bench-client", "--target=", "--", "unexpected"}, + } { + t.Run(strings.Join(args, " "), func(t *testing.T) { + err := run(context.Background(), args) + if err == nil || !strings.Contains(err.Error(), "positional arguments") { + t.Fatalf("want argument rejection before startup validation, got %v", err) + } + }) + } +} diff --git a/cmd/autocar/config_file.go b/cmd/autocar/config_file.go index 7d44b68..fe5c203 100644 --- a/cmd/autocar/config_file.go +++ b/cmd/autocar/config_file.go @@ -22,12 +22,9 @@ const maxCommandConfigSize = 64 << 10 // performs its normal semantic and credential validation after parsing. func parseFlagsWithConfig(fs *flag.FlagSet, args []string) error { configPath := fs.String("config", "", "JSON options file; CLI overrides it, file paths are relative to its directory") - if err := fs.Parse(args); err != nil { + if err := parseCommandFlags(fs, args); err != nil { return err } - if fs.NArg() != 0 { - return errors.New("positional arguments are not supported; use named options") - } if *configPath == "" { return nil } diff --git a/cmd/autocar/init.go b/cmd/autocar/init.go index dc99560..199b434 100644 --- a/cmd/autocar/init.go +++ b/cmd/autocar/init.go @@ -31,12 +31,9 @@ func runInitWith(args []string, output io.Writer) error { serverName := fs.String("server-name", "", "certificate DNS name or IP; defaults to the relay host") directory := fs.String("out", "autocar-config", "new private output directory; must not already exist") days := fs.Int("days", 365, "self-signed certificate validity in days (1-1825)") - if err := fs.Parse(args); err != nil { + if err := parseCommandFlags(fs, args); err != nil { return err } - if fs.NArg() != 0 { - return errors.New("init does not accept positional arguments") - } if strings.TrimSpace(*directory) == "" { return errors.New("--out must name a new directory") } diff --git a/cmd/autocar/token.go b/cmd/autocar/token.go index 6e82066..5d9b9f5 100644 --- a/cmd/autocar/token.go +++ b/cmd/autocar/token.go @@ -14,7 +14,7 @@ func runToken(args []string) error { fs := flag.NewFlagSet("token", flag.ContinueOnError) output := fs.String("out", "", "write to a new 0600 file instead of stdout") bytesCount := fs.Int("bytes", 32, "random byte count (16-128)") - if err := fs.Parse(args); err != nil { + if err := parseCommandFlags(fs, args); err != nil { return err } if *bytesCount < 16 || *bytesCount > 128 { diff --git a/docs/DEPLOYMENT.md b/docs/DEPLOYMENT.md index 30315ef..9d8d932 100644 --- a/docs/DEPLOYMENT.md +++ b/docs/DEPLOYMENT.md @@ -9,6 +9,12 @@ relay and an `auto` client; installing v1.0.1 does not enable web-cover. ## 1. Build and create credentials +Current source builds reject unexpected positional operands in option-based +commands, including operands after `--`; use named options for every setting. +Credential commands (`token`, `cert`, `init`) reject these inputs before creating +files, and benchmark commands reject them before startup. Normal flag syntax and +`-h`/`--help` remain supported. This does not add `--config` to these commands. + ### Generate a new paired bundle With a build containing `init` and `--check` (not the published v1.0.1 binary), @@ -429,8 +435,8 @@ separately from QUIC circuit health. ### Reusable configuration files (after v1.0.1) Builds containing the configuration-file change support `--config` for `client`, -`server`, and `doctor`; the published v1.0.1 binary does not. Existing CLI-only -commands are unchanged. Copy [examples/client.json](../examples/client.json) +`server`, and `doctor`; the published v1.0.1 binary does not. CLI-only commands do +not gain `--config` support. Copy [examples/client.json](../examples/client.json) and [examples/server.json](../examples/server.json) into your credential directory and adjust addresses and filenames. These files contain references, not secrets; keep the existing token and private-key permission requirements.