mirror of
https://github.com/aykhans/sarin.git
synced 2026-01-13 20:11:21 +00:00
286 lines
9.3 KiB
Go
286 lines
9.3 KiB
Go
package config
|
|
|
|
import (
|
|
"flag"
|
|
"fmt"
|
|
"net/url"
|
|
"os"
|
|
"strings"
|
|
"time"
|
|
|
|
"go.aykhans.me/sarin/internal/types"
|
|
versionpkg "go.aykhans.me/sarin/internal/version"
|
|
"go.aykhans.me/utils/common"
|
|
)
|
|
|
|
const cliUsageText = `Usage:
|
|
sarin [flags]
|
|
|
|
Simple usage:
|
|
sarin -U https://example.com -d 1m
|
|
|
|
Usage with all flags:
|
|
sarin -s -q -z -o json -f ./config.yaml -c 50 -r 100_000 -d 2m30s \
|
|
-U https://example.com \
|
|
-M POST \
|
|
-V "sharedUUID={{ fakeit_UUID }}" \
|
|
-B '{"product": "car"}' \
|
|
-P "id={{ .Values.sharedUUID }}" \
|
|
-H "User-Agent: {{ fakeit_UserAgent }}" -H "Accept: */*" \
|
|
-C "token={{ .Values.sharedUUID }}" \
|
|
-X "http://proxy.example.com" \
|
|
-T 3s \
|
|
-I
|
|
|
|
Flags:
|
|
General Config:
|
|
-h, -help Help for sarin
|
|
-v, -version Version for sarin
|
|
-s, -show-config bool Show the final config after parsing all sources (default %v)
|
|
-f, -config-file string Path to the config file (local file / http URL)
|
|
-c, -concurrency uint Number of concurrent requests (default %d)
|
|
-r, -requests uint Number of total requests
|
|
-d, -duration time Maximum duration for the test (e.g. 30s, 1m, 5h)
|
|
-q, -quiet bool Hide the progress bar and runtime logs (default %v)
|
|
-o, -output string Output format (possible values: table, json, yaml, none) (default '%v')
|
|
-z, -dry-run bool Run without sending requests (default %v)
|
|
|
|
Request Config:
|
|
-U, -url string Target URL for the request
|
|
-M, -method []string HTTP method for the request (default %s)
|
|
-B, -body []string Body for the request (e.g. "body text")
|
|
-P, -param []string URL 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")
|
|
-V, -values []string List of values for templating (e.g. "key1=value1")
|
|
-T, -timeout time Timeout for the request (e.g. 400ms, 3s, 1m10s) (default %v)
|
|
-I, -insecure bool Skip SSL/TLS certificate verification (default %v)`
|
|
|
|
var _ IParser = ConfigCLIParser{}
|
|
|
|
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("sarin", flag.ExitOnError)
|
|
|
|
flagSet.Usage = func() { parser.PrintHelp() }
|
|
|
|
var (
|
|
config = &Config{}
|
|
|
|
// General config
|
|
version bool
|
|
showConfig bool
|
|
configFiles = stringSliceArg{}
|
|
concurrency uint
|
|
requestCount uint64
|
|
duration time.Duration
|
|
quiet bool
|
|
output string
|
|
dryRun bool
|
|
|
|
// Request config
|
|
urlInput string
|
|
methods = stringSliceArg{}
|
|
bodies = stringSliceArg{}
|
|
params = stringSliceArg{}
|
|
headers = stringSliceArg{}
|
|
cookies = stringSliceArg{}
|
|
proxies = stringSliceArg{}
|
|
values = stringSliceArg{}
|
|
timeout time.Duration
|
|
insecure bool
|
|
)
|
|
|
|
{
|
|
// General config
|
|
flagSet.BoolVar(&version, "version", false, "Version for sarin")
|
|
flagSet.BoolVar(&version, "v", false, "Version for sarin")
|
|
|
|
flagSet.BoolVar(&showConfig, "show-config", false, "Show the final config after parsing all sources")
|
|
flagSet.BoolVar(&showConfig, "s", false, "Show the final config after parsing all sources")
|
|
|
|
flagSet.Var(&configFiles, "config-file", "Path to the config file")
|
|
flagSet.Var(&configFiles, "f", "Path to the config file")
|
|
|
|
flagSet.UintVar(&concurrency, "concurrency", 0, "Number of concurrent requests")
|
|
flagSet.UintVar(&concurrency, "c", 0, "Number of concurrent requests")
|
|
|
|
flagSet.Uint64Var(&requestCount, "requests", 0, "Number of total requests")
|
|
flagSet.Uint64Var(&requestCount, "r", 0, "Number of total requests")
|
|
|
|
flagSet.DurationVar(&duration, "duration", 0, "Maximum duration for the test")
|
|
flagSet.DurationVar(&duration, "d", 0, "Maximum duration for the test")
|
|
|
|
flagSet.BoolVar(&quiet, "quiet", false, "Hide the progress bar and runtime logs")
|
|
flagSet.BoolVar(&quiet, "q", false, "Hide the progress bar and runtime logs")
|
|
|
|
flagSet.StringVar(&output, "output", "", "Output format (possible values: table, json, yaml, none)")
|
|
flagSet.StringVar(&output, "o", "", "Output format (possible values: table, json, yaml, none)")
|
|
|
|
flagSet.BoolVar(&dryRun, "dry-run", false, "Run without sending requests")
|
|
flagSet.BoolVar(&dryRun, "z", false, "Run without sending requests")
|
|
|
|
// Request config
|
|
flagSet.StringVar(&urlInput, "url", "", "Target URL for the request")
|
|
flagSet.StringVar(&urlInput, "U", "", "Target URL for the request")
|
|
|
|
flagSet.Var(&methods, "method", "HTTP method for the request")
|
|
flagSet.Var(&methods, "M", "HTTP method for the request")
|
|
|
|
flagSet.Var(&bodies, "body", "Body for the request")
|
|
flagSet.Var(&bodies, "B", "Body for the request")
|
|
|
|
flagSet.Var(¶ms, "param", "URL parameter for the request")
|
|
flagSet.Var(¶ms, "P", "URL parameter for the request")
|
|
|
|
flagSet.Var(&headers, "header", "Header for the request")
|
|
flagSet.Var(&headers, "H", "Header for the request")
|
|
|
|
flagSet.Var(&cookies, "cookie", "Cookie for the request")
|
|
flagSet.Var(&cookies, "C", "Cookie for the request")
|
|
|
|
flagSet.Var(&proxies, "proxy", "Proxy for the request")
|
|
flagSet.Var(&proxies, "X", "Proxy for the request")
|
|
|
|
flagSet.Var(&values, "values", "List of values for templating")
|
|
flagSet.Var(&values, "V", "List of values for templating")
|
|
|
|
flagSet.DurationVar(&timeout, "timeout", 0, "Timeout for the request (e.g. 400ms, 15s, 1m10s)")
|
|
flagSet.DurationVar(&timeout, "T", 0, "Timeout for the request (e.g. 400ms, 15s, 1m10s)")
|
|
|
|
flagSet.BoolVar(&insecure, "insecure", false, "Skip SSL/TLS certificate verification")
|
|
flagSet.BoolVar(&insecure, "I", false, "Skip SSL/TLS certificate verification")
|
|
}
|
|
|
|
// 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 `sarin` 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)
|
|
}
|
|
|
|
if version {
|
|
fmt.Printf("Version: %s\nGit Commit: %s\nBuild Date: %s\nGo Version: %s\n",
|
|
versionpkg.Version, versionpkg.GitCommit, versionpkg.BuildDate, versionpkg.GoVersion)
|
|
os.Exit(0)
|
|
}
|
|
|
|
var fieldParseErrors []types.FieldParseError
|
|
// Iterate over flags that were explicitly set on the command line.
|
|
flagSet.Visit(func(flagVar *flag.Flag) {
|
|
switch flagVar.Name {
|
|
// General config
|
|
case "show-config", "s":
|
|
config.ShowConfig = common.ToPtr(showConfig)
|
|
case "config-file", "f":
|
|
for _, configFile := range configFiles {
|
|
config.Files = append(config.Files, *types.ParseConfigFile(configFile))
|
|
}
|
|
case "concurrency", "c":
|
|
config.Concurrency = common.ToPtr(concurrency)
|
|
case "requests", "r":
|
|
config.Requests = common.ToPtr(requestCount)
|
|
case "duration", "d":
|
|
config.Duration = common.ToPtr(duration)
|
|
case "quiet", "q":
|
|
config.Quiet = common.ToPtr(quiet)
|
|
case "output", "o":
|
|
config.Output = common.ToPtr(ConfigOutputType(output))
|
|
case "dry-run", "z":
|
|
config.DryRun = common.ToPtr(dryRun)
|
|
|
|
// Request config
|
|
case "url", "U":
|
|
urlParsed, err := url.Parse(urlInput)
|
|
if err != nil {
|
|
fieldParseErrors = append(fieldParseErrors, types.NewFieldParseError("url", urlInput, err))
|
|
} else {
|
|
config.URL = urlParsed
|
|
}
|
|
case "method", "M":
|
|
config.Methods = append(config.Methods, methods...)
|
|
case "body", "B":
|
|
config.Bodies = append(config.Bodies, bodies...)
|
|
case "param", "P":
|
|
config.Params.Parse(params...)
|
|
case "header", "H":
|
|
config.Headers.Parse(headers...)
|
|
case "cookie", "C":
|
|
config.Cookies.Parse(cookies...)
|
|
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),
|
|
)
|
|
}
|
|
}
|
|
case "values", "V":
|
|
config.Values = append(config.Values, values...)
|
|
case "timeout", "T":
|
|
config.Timeout = common.ToPtr(timeout)
|
|
case "insecure", "I":
|
|
config.Insecure = common.ToPtr(insecure)
|
|
}
|
|
})
|
|
|
|
if len(fieldParseErrors) > 0 {
|
|
return nil, types.NewFieldParseErrors(fieldParseErrors)
|
|
}
|
|
|
|
return config, nil
|
|
}
|
|
|
|
func (parser ConfigCLIParser) PrintHelp() {
|
|
fmt.Printf(
|
|
cliUsageText+"\n",
|
|
Defaults.ShowConfig,
|
|
Defaults.Concurrency,
|
|
Defaults.Quiet,
|
|
Defaults.Output,
|
|
Defaults.DryRun,
|
|
|
|
Defaults.Method,
|
|
Defaults.RequestTimeout,
|
|
Defaults.Insecure,
|
|
)
|
|
}
|