v1.0.0: here we go again

This commit is contained in:
2026-01-10 17:06:25 +04:00
parent 25d4762a3c
commit 2d7ba34cb8
68 changed files with 6805 additions and 4548 deletions

285
internal/config/cli.go Normal file
View File

@@ -0,0 +1,285 @@
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(&params, "param", "URL parameter for the request")
flagSet.Var(&params, "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,
)
}