mirror of
https://github.com/aykhans/sarin.git
synced 2026-02-28 06:49:13 +00:00
add scripting js/lua
This commit is contained in:
@@ -17,20 +17,7 @@ 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
|
||||
sarin -U https://example.com -r 1
|
||||
|
||||
Flags:
|
||||
General Config:
|
||||
@@ -55,7 +42,9 @@ Flags:
|
||||
-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)`
|
||||
-I, -insecure bool Skip SSL/TLS certificate verification (default %v)
|
||||
-lua []string Lua script for request transformation (inline or @file/@url)
|
||||
-js []string JavaScript script for request transformation (inline or @file/@url)`
|
||||
|
||||
var _ IParser = ConfigCLIParser{}
|
||||
|
||||
@@ -106,16 +95,18 @@ func (parser ConfigCLIParser) Parse() (*Config, error) {
|
||||
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
|
||||
urlInput string
|
||||
methods = stringSliceArg{}
|
||||
bodies = stringSliceArg{}
|
||||
params = stringSliceArg{}
|
||||
headers = stringSliceArg{}
|
||||
cookies = stringSliceArg{}
|
||||
proxies = stringSliceArg{}
|
||||
values = stringSliceArg{}
|
||||
timeout time.Duration
|
||||
insecure bool
|
||||
luaScripts = stringSliceArg{}
|
||||
jsScripts = stringSliceArg{}
|
||||
)
|
||||
|
||||
{
|
||||
@@ -177,6 +168,10 @@ func (parser ConfigCLIParser) Parse() (*Config, error) {
|
||||
|
||||
flagSet.BoolVar(&insecure, "insecure", false, "Skip SSL/TLS certificate verification")
|
||||
flagSet.BoolVar(&insecure, "I", false, "Skip SSL/TLS certificate verification")
|
||||
|
||||
flagSet.Var(&luaScripts, "lua", "Lua script for request transformation (inline or @file/@url)")
|
||||
|
||||
flagSet.Var(&jsScripts, "js", "JavaScript script for request transformation (inline or @file/@url)")
|
||||
}
|
||||
|
||||
// Parse the specific arguments provided to the parser, skipping the program name.
|
||||
@@ -259,6 +254,10 @@ func (parser ConfigCLIParser) Parse() (*Config, error) {
|
||||
config.Timeout = common.ToPtr(timeout)
|
||||
case "insecure", "I":
|
||||
config.Insecure = common.ToPtr(insecure)
|
||||
case "lua":
|
||||
config.Lua = append(config.Lua, luaScripts...)
|
||||
case "js":
|
||||
config.Js = append(config.Js, jsScripts...)
|
||||
}
|
||||
})
|
||||
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
package config
|
||||
|
||||
import (
|
||||
"context"
|
||||
"errors"
|
||||
"fmt"
|
||||
"net/url"
|
||||
@@ -16,6 +17,7 @@ import (
|
||||
"github.com/charmbracelet/glamour/styles"
|
||||
"github.com/charmbracelet/lipgloss"
|
||||
"github.com/charmbracelet/x/term"
|
||||
"go.aykhans.me/sarin/internal/script"
|
||||
"go.aykhans.me/sarin/internal/types"
|
||||
"go.aykhans.me/sarin/internal/version"
|
||||
"go.aykhans.me/utils/common"
|
||||
@@ -87,6 +89,8 @@ type Config struct {
|
||||
Bodies []string `yaml:"bodies,omitempty"`
|
||||
Proxies types.Proxies `yaml:"proxies,omitempty"`
|
||||
Values []string `yaml:"values,omitempty"`
|
||||
Lua []string `yaml:"lua,omitempty"`
|
||||
Js []string `yaml:"js,omitempty"`
|
||||
}
|
||||
|
||||
func NewConfig() *Config {
|
||||
@@ -219,6 +223,8 @@ func (config Config) MarshalYAML() (any, error) {
|
||||
}
|
||||
|
||||
addStringSlice(content, "values", config.Values, false)
|
||||
addStringSlice(content, "lua", config.Lua, false)
|
||||
addStringSlice(content, "js", config.Js, false)
|
||||
|
||||
return root, nil
|
||||
}
|
||||
@@ -323,6 +329,12 @@ func (config *Config) Merge(newConfig *Config) {
|
||||
if len(newConfig.Values) != 0 {
|
||||
config.Values = append(config.Values, newConfig.Values...)
|
||||
}
|
||||
if len(newConfig.Lua) != 0 {
|
||||
config.Lua = append(config.Lua, newConfig.Lua...)
|
||||
}
|
||||
if len(newConfig.Js) != 0 {
|
||||
config.Js = append(config.Js, newConfig.Js...)
|
||||
}
|
||||
}
|
||||
|
||||
func (config *Config) SetDefaults() {
|
||||
@@ -465,6 +477,44 @@ func (config Config) Validate() error {
|
||||
}
|
||||
}
|
||||
|
||||
// Create a context with timeout for script validation (loading from URLs)
|
||||
scriptCtx, scriptCancel := context.WithTimeout(context.Background(), 30*time.Second)
|
||||
defer scriptCancel()
|
||||
|
||||
for i, scriptSrc := range config.Lua {
|
||||
if err := validateScriptSource(scriptSrc); err != nil {
|
||||
validationErrors = append(
|
||||
validationErrors,
|
||||
types.NewFieldValidationError(fmt.Sprintf("Lua[%d]", i), scriptSrc, err),
|
||||
)
|
||||
continue
|
||||
}
|
||||
// Validate script syntax
|
||||
if err := script.ValidateScript(scriptCtx, scriptSrc, script.EngineTypeLua); err != nil {
|
||||
validationErrors = append(
|
||||
validationErrors,
|
||||
types.NewFieldValidationError(fmt.Sprintf("Lua[%d]", i), scriptSrc, err),
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
for i, scriptSrc := range config.Js {
|
||||
if err := validateScriptSource(scriptSrc); err != nil {
|
||||
validationErrors = append(
|
||||
validationErrors,
|
||||
types.NewFieldValidationError(fmt.Sprintf("Js[%d]", i), scriptSrc, err),
|
||||
)
|
||||
continue
|
||||
}
|
||||
// Validate script syntax
|
||||
if err := script.ValidateScript(scriptCtx, scriptSrc, script.EngineTypeJavaScript); err != nil {
|
||||
validationErrors = append(
|
||||
validationErrors,
|
||||
types.NewFieldValidationError(fmt.Sprintf("Js[%d]", i), scriptSrc, err),
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
templateErrors := ValidateTemplates(&config)
|
||||
validationErrors = append(validationErrors, templateErrors...)
|
||||
|
||||
@@ -582,6 +632,51 @@ func parseConfigFile(configFile types.ConfigFile, maxDepth int) (*Config, error)
|
||||
return fileConfig, nil
|
||||
}
|
||||
|
||||
// validateScriptSource validates a script source string.
|
||||
// Scripts can be:
|
||||
// - Inline script: any string not starting with "@"
|
||||
// - Escaped "@": strings starting with "@@" (literal "@" at start)
|
||||
// - File reference: "@/path/to/file" or "@./relative/path"
|
||||
// - URL reference: "@http://..." or "@https://..."
|
||||
func validateScriptSource(script string) error {
|
||||
// Empty script is invalid
|
||||
if script == "" {
|
||||
return errors.New("script cannot be empty")
|
||||
}
|
||||
|
||||
// Not a file/URL reference - it's an inline script
|
||||
if !strings.HasPrefix(script, "@") {
|
||||
return nil
|
||||
}
|
||||
|
||||
// Escaped @ - it's an inline script starting with literal @
|
||||
if strings.HasPrefix(script, "@@") {
|
||||
return nil
|
||||
}
|
||||
|
||||
// It's a file or URL reference - validate the source
|
||||
source := script[1:] // Remove the @ prefix
|
||||
|
||||
if source == "" {
|
||||
return errors.New("script source cannot be empty after @")
|
||||
}
|
||||
|
||||
// Check if it's a URL
|
||||
if strings.HasPrefix(source, "http://") || strings.HasPrefix(source, "https://") {
|
||||
parsedURL, err := url.Parse(source)
|
||||
if err != nil {
|
||||
return fmt.Errorf("invalid URL: %w", err)
|
||||
}
|
||||
if parsedURL.Host == "" {
|
||||
return errors.New("URL must have a host")
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
// It's a file path - basic validation (not empty, checked above)
|
||||
return nil
|
||||
}
|
||||
|
||||
func printParseErrors(parserName string, errors ...types.FieldParseError) {
|
||||
for _, fieldErr := range errors {
|
||||
if fieldErr.Value == "" {
|
||||
|
||||
@@ -216,6 +216,14 @@ func (parser ConfigENVParser) Parse() (*Config, error) {
|
||||
config.Values = []string{values}
|
||||
}
|
||||
|
||||
if lua := parser.getEnv("LUA"); lua != "" {
|
||||
config.Lua = []string{lua}
|
||||
}
|
||||
|
||||
if js := parser.getEnv("JS"); js != "" {
|
||||
config.Js = []string{js}
|
||||
}
|
||||
|
||||
if len(fieldParseErrors) > 0 {
|
||||
return nil, types.NewFieldParseErrors(fieldParseErrors)
|
||||
}
|
||||
|
||||
@@ -202,6 +202,8 @@ type configYAML struct {
|
||||
Bodies stringOrSliceField `yaml:"body"`
|
||||
Proxies stringOrSliceField `yaml:"proxy"`
|
||||
Values stringOrSliceField `yaml:"values"`
|
||||
Lua stringOrSliceField `yaml:"lua"`
|
||||
Js stringOrSliceField `yaml:"js"`
|
||||
}
|
||||
|
||||
// ParseYAML parses YAML config file arguments into a Config object.
|
||||
@@ -246,6 +248,8 @@ func (parser ConfigFileParser) ParseYAML(data []byte) (*Config, error) {
|
||||
}
|
||||
config.Bodies = append(config.Bodies, parsedData.Bodies...)
|
||||
config.Values = append(config.Values, parsedData.Values...)
|
||||
config.Lua = append(config.Lua, parsedData.Lua...)
|
||||
config.Js = append(config.Js, parsedData.Js...)
|
||||
|
||||
if len(parsedData.ConfigFiles) > 0 {
|
||||
for _, configFile := range parsedData.ConfigFiles {
|
||||
|
||||
Reference in New Issue
Block a user