package config import ( "errors" "flag" "fmt" "net/url" "strings" "time" "github.com/aykhans/dodo/pkg/types" "github.com/aykhans/dodo/pkg/utils" ) const cliUsageText = `Usage: dodo [flags] Examples: Simple usage: dodo -u https://example.com -o 1m Usage with config file: dodo -f /path/to/config/file/config.json Usage with all flags: dodo -f /path/to/config/file/config.json \ -u https://example.com -m POST \ -d 10 -r 1000 -o 3m -t 3s \ -b "body1" -body "body2" \ -H "header1:value1" -header "header2:value2" \ -p "param1=value1" -param "param2=value2" \ -c "cookie1=value1" -cookie "cookie2=value2" \ -x "http://proxy.example.com:8080" -proxy "socks5://proxy2.example.com:8080" \ -skip-verify -y Flags: -h, -help help for dodo -v, -version version for dodo -y, -yes bool Answer yes to all questions (default %v) -f, -config-file string Path to the local config file or http(s) URL of the config file -d, -dodos uint Number of dodos(threads) (default %d) -r, -requests uint Number of total requests -o, -duration Time Maximum duration for the test (e.g. 30s, 1m, 5h) -t, -timeout Time Timeout for each request (e.g. 400ms, 15s, 1m10s) (default %v) -u, -url string URL for stress testing -m, -method string HTTP Method for the request (default %s) -b, -body [string] Body for the request (e.g. "body text") -p, -param [string] Parameter for the request (e.g. "key1=value1") -H, -header [string] Header for the request (e.g. "key1: value1") -c, -cookie [string] Cookie for the request (e.g. "key1=value1") -x, -proxy [string] Proxy for the request (e.g. "http://proxy.example.com:8080") -skip-verify bool Skip SSL/TLS certificate verification (default %v)` type ConfigCLIParser struct { args []string } func NewConfigCLIParser(args []string) *ConfigCLIParser { if args == nil { args = []string{} } return &ConfigCLIParser{args: args} } type stringSliceArg []string func (arg *stringSliceArg) String() string { return strings.Join(*arg, ",") } func (arg *stringSliceArg) Set(value string) error { *arg = append(*arg, value) return nil } // Parse parses command-line arguments into a Config object. // It can return the following errors: // - types.ErrCLINoArgs // - types.CLIUnexpectedArgsError // - types.FieldParseErrors func (parser *ConfigCLIParser) Parse() (*Config, error) { flagSet := flag.NewFlagSet("dodo", flag.ExitOnError) flagSet.Usage = func() { parser.PrintHelp() } var ( config = &Config{} configFiles = stringSliceArg{} yes bool skipVerify bool method string urlInput string dodosCount uint requestCount uint duration time.Duration timeout time.Duration params = stringSliceArg{} headers = stringSliceArg{} cookies = stringSliceArg{} bodies = stringSliceArg{} proxies = stringSliceArg{} ) { flagSet.Var(&configFiles, "config-file", "Config file") flagSet.Var(&configFiles, "f", "Config file") flagSet.BoolVar(&yes, "yes", false, "Answer yes to all questions") flagSet.BoolVar(&yes, "y", false, "Answer yes to all questions") flagSet.BoolVar(&skipVerify, "skip-verify", false, "Skip SSL/TLS certificate verification") flagSet.StringVar(&method, "method", "", "HTTP Method") flagSet.StringVar(&method, "m", "", "HTTP Method") flagSet.StringVar(&urlInput, "url", "", "URL to send the request") flagSet.StringVar(&urlInput, "u", "", "URL to send the request") flagSet.UintVar(&dodosCount, "dodos", 0, "Number of dodos(threads)") flagSet.UintVar(&dodosCount, "d", 0, "Number of dodos(threads)") flagSet.UintVar(&requestCount, "requests", 0, "Number of total requests") flagSet.UintVar(&requestCount, "r", 0, "Number of total requests") flagSet.DurationVar(&duration, "duration", 0, "Maximum duration of the test") flagSet.DurationVar(&duration, "o", 0, "Maximum duration of the test") flagSet.DurationVar(&timeout, "timeout", 0, "Timeout for each request (e.g. 400ms, 15s, 1m10s)") flagSet.DurationVar(&timeout, "t", 0, "Timeout for each request (e.g. 400ms, 15s, 1m10s)") flagSet.Var(¶ms, "param", "URL parameter to send with the request") flagSet.Var(¶ms, "p", "URL parameter to send with the request") flagSet.Var(&headers, "header", "Header to send with the request") flagSet.Var(&headers, "H", "Header to send with the request") flagSet.Var(&cookies, "cookie", "Cookie to send with the request") flagSet.Var(&cookies, "c", "Cookie to send with the request") flagSet.Var(&bodies, "body", "Body to send with the request") flagSet.Var(&bodies, "b", "Body to send with the request") flagSet.Var(&proxies, "proxy", "Proxy to use for the request") flagSet.Var(&proxies, "x", "Proxy to use for the request") } // Parse the specific arguments provided to the parser, skipping the program name. if err := flagSet.Parse(parser.args[1:]); err != nil { panic(err) } // Check if no flags were set and no non-flag arguments were provided. // This covers cases where `dodo` is run without any meaningful arguments. if flagSet.NFlag() == 0 && len(flagSet.Args()) == 0 { return nil, types.ErrCLINoArgs } // Check for any unexpected non-flag arguments remaining after parsing. if args := flagSet.Args(); len(args) > 0 { return nil, types.NewCLIUnexpectedArgsError(args) } var fieldParseErrors []types.FieldParseError // Iterate over flags that were explicitly set on the command line. flagSet.Visit(func(flagVar *flag.Flag) { switch flagVar.Name { case "config-file", "f": for i, configFile := range configFiles { configFileParsed, err := types.ParseConfigFile(configFile) _ = utils.HandleErrorOrDie(err, utils.OnSentinelError(types.ErrConfigFileExtensionNotFound, func(err error) error { fieldParseErrors = append( fieldParseErrors, *types.NewFieldParseError( fmt.Sprintf("config-file[%d]", i), configFile, errors.New("file extension not found"), ), ) return nil }), utils.OnCustomError(func(err types.RemoteConfigFileParseError) error { fieldParseErrors = append( fieldParseErrors, *types.NewFieldParseError( fmt.Sprintf("config-file[%d]", i), configFile, fmt.Errorf("parse error: %w", err), ), ) return nil }), utils.OnCustomError(func(err types.UnknownConfigFileTypeError) error { fieldParseErrors = append( fieldParseErrors, *types.NewFieldParseError( fmt.Sprintf("config-file[%d]", i), configFile, fmt.Errorf("file type '%s' not supported (supported types: %s)", err.Type, types.ConfigFileTypeYAML), ), ) return nil }), ) if err == nil { config.Files = append(config.Files, *configFileParsed) } } case "yes", "y": config.Yes = utils.ToPtr(yes) case "skip-verify": config.SkipVerify = utils.ToPtr(skipVerify) case "method", "m": config.Method = utils.ToPtr(method) case "url", "u": urlParsed, err := url.Parse(urlInput) if err != nil { fieldParseErrors = append(fieldParseErrors, *types.NewFieldParseError("url", urlInput, err)) } else { config.URL = urlParsed } case "dodos", "d": config.DodosCount = utils.ToPtr(dodosCount) case "requests", "r": config.RequestCount = utils.ToPtr(requestCount) case "duration", "o": config.Duration = utils.ToPtr(duration) case "timeout", "t": config.Timeout = utils.ToPtr(timeout) case "param", "p": config.Params.Parse(params...) case "header", "H": config.Headers.Parse(headers...) case "cookie", "c": config.Cookies.Parse(cookies...) case "body", "b": config.Bodies.Parse(bodies...) case "proxy", "x": for i, proxy := range proxies { err := config.Proxies.Parse(proxy) if err != nil { fieldParseErrors = append( fieldParseErrors, *types.NewFieldParseError(fmt.Sprintf("proxy[%d]", i), proxy, err), ) } } } }) if len(fieldParseErrors) > 0 { return nil, types.NewFieldParseErrors(fieldParseErrors) } return config, nil } func (parser *ConfigCLIParser) PrintHelp() { fmt.Printf( cliUsageText+"\n", Defaults.Yes, Defaults.DodosCount, Defaults.RequestTimeout, Defaults.Method, Defaults.SkipVerify, ) } // CLIYesOrNoReader reads a yes or no answer from the command line. // It prompts the user with the given message and default value, // and returns true if the user answers "y" or "Y", and false otherwise. // If there is an error while reading the input, it returns false. // If the user simply presses enter without providing any input, // it returns the default value specified by the `def` parameter. func CLIYesOrNoReader(message string, def bool) bool { var answer string defaultMessage := "Y/n" if !def { defaultMessage = "y/N" } fmt.Printf("%s [%s]: ", message, defaultMessage) if _, err := fmt.Scanln(&answer); err != nil { if err.Error() == "unexpected newline" { return def } return false } if answer == "" { return def } return answer == "y" || answer == "Y" }