mirror of
https://github.com/aykhans/dodo.git
synced 2025-09-01 00:53:34 +00:00
284 lines
8.9 KiB
Go
284 lines
8.9 KiB
Go
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
|
|
configFile *types.ConfigFile
|
|
}
|
|
|
|
func NewConfigCLIParser(args []string) *ConfigCLIParser {
|
|
if args == nil {
|
|
args = []string{}
|
|
}
|
|
return &ConfigCLIParser{args: args}
|
|
}
|
|
|
|
func (parser ConfigCLIParser) GetConfigFile() *types.ConfigFile {
|
|
return parser.configFile
|
|
}
|
|
|
|
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{}
|
|
configFile string
|
|
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.StringVar(&configFile, "config-file", "", "Config file")
|
|
flagSet.StringVar(&configFile, "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":
|
|
var err error
|
|
parser.configFile, err = types.ParseConfigFile(configFile)
|
|
_ = utils.HandleErrorOrDie(err,
|
|
utils.OnSentinelError(types.ErrConfigFileExtensionNotFound, func(err error) error {
|
|
fieldParseErrors = append(fieldParseErrors, *types.NewFieldParseError("config-file", errors.New("file extension not found")))
|
|
return nil
|
|
}),
|
|
utils.OnCustomError(func(err types.RemoteConfigFileParseError) error {
|
|
fieldParseErrors = append(fieldParseErrors, *types.NewFieldParseError("config-file", fmt.Errorf("parse error: %w", err)))
|
|
return nil
|
|
}),
|
|
utils.OnCustomError(func(err types.UnknownConfigFileTypeError) error {
|
|
fieldParseErrors = append(
|
|
fieldParseErrors,
|
|
*types.NewFieldParseError(
|
|
"config-file",
|
|
fmt.Errorf("file type '%s' not supported (supported types: %s)", err.Type, types.ConfigFileTypeYAML),
|
|
),
|
|
)
|
|
return nil
|
|
}),
|
|
)
|
|
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", 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), 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"
|
|
}
|