mirror of
https://github.com/aykhans/sarin.git
synced 2026-10-13 21:28:52 +00:00
Compare commits
2
Commits
5d91141f80
...
c32949c8e0
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
c32949c8e0
|
||
|
|
a30c862936
|
No files matched your search
+1
-1
@@ -75,7 +75,7 @@ func main() {
|
||||
}),
|
||||
)
|
||||
|
||||
srn.Start(ctx, stopCtrl)
|
||||
srn.Start(stopCtrl)
|
||||
|
||||
switch *combinedConfig.Output {
|
||||
case config.ConfigOutputTypeNone:
|
||||
|
||||
@@ -456,7 +456,7 @@ function transform(req)
|
||||
end
|
||||
```
|
||||
|
||||
> **Note:** Header, parameter, and cookie values can be a single string or a table (array) for multiple values per key (e.g. `{"val1", "val2"}`).
|
||||
> **Note:** Header, parameter, and cookie values can be a single string or a table (array) for multiple values per key (e.g. `{"val1", "val2"}`). Numbers and booleans are converted to text, other values are skipped.
|
||||
|
||||
> **Note:** Lua scripts can send their own HTTP requests with `http` and work with JSON through the global `json` table. See [HTTP Requests in Scripts](#http-requests-in-scripts).
|
||||
|
||||
@@ -523,7 +523,7 @@ function transform(req) {
|
||||
}
|
||||
```
|
||||
|
||||
> **Note:** Header, parameter, and cookie values can be a single string or an array for multiple values per key (e.g. `["val1", "val2"]`).
|
||||
> **Note:** Header, parameter, and cookie values can be a single string or an array for multiple values per key (e.g. `["val1", "val2"]`). Numbers and booleans are converted to text, other values are skipped.
|
||||
|
||||
> **Note:** JavaScript scripts can send their own HTTP requests with `http`. See [HTTP Requests in Scripts](#http-requests-in-scripts).
|
||||
|
||||
|
||||
+19
-13
@@ -199,6 +199,10 @@ type sarin struct {
|
||||
logError bool
|
||||
logFile string
|
||||
|
||||
// jobsCtx ends the run, either when the duration expires or on Ctrl+C.
|
||||
jobsCtx context.Context //nolint:containedctx
|
||||
jobsCancel context.CancelFunc
|
||||
|
||||
hostClients []*fasthttp.HostClient
|
||||
scriptHTTPClients []*scriptHTTPClient
|
||||
responses *SarinResponseData
|
||||
@@ -272,15 +276,20 @@ func NewSarin(
|
||||
|
||||
scriptChain := script.NewChain(luaSources, jsSources)
|
||||
|
||||
jobsCtx, jobsCancel := context.WithCancel(ctx)
|
||||
|
||||
var scriptHTTPClients []*scriptHTTPClient
|
||||
if !scriptChain.IsEmpty() {
|
||||
scriptHTTPClients, err = newScriptHTTPClients(ctx, proxyURLs, workers)
|
||||
scriptHTTPClients, err = newScriptHTTPClients(jobsCtx, proxyURLs, workers)
|
||||
if err != nil {
|
||||
jobsCancel()
|
||||
return nil, err
|
||||
}
|
||||
}
|
||||
|
||||
srn := &sarin{
|
||||
jobsCtx: jobsCtx,
|
||||
jobsCancel: jobsCancel,
|
||||
workers: workers,
|
||||
requestURL: requestURL,
|
||||
methods: methods,
|
||||
@@ -316,9 +325,7 @@ func (s sarin) GetResponses() *SarinResponseData {
|
||||
return s.responses
|
||||
}
|
||||
|
||||
func (s sarin) Start(ctx context.Context, stopCtrl *StopController) {
|
||||
jobsCtx, jobsCancel := context.WithCancel(ctx)
|
||||
|
||||
func (s sarin) Start(stopCtrl *StopController) {
|
||||
var workersWG sync.WaitGroup
|
||||
jobsCh := make(chan struct{}, max(s.workers, 1))
|
||||
|
||||
@@ -382,15 +389,14 @@ func (s sarin) Start(ctx context.Context, stopCtrl *StopController) {
|
||||
s.startWorkers(&workersWG, jobsCh, &counter, sendLog, sendRespLog)
|
||||
|
||||
if runTUI {
|
||||
//nolint:contextcheck // streamCtx must remain active until all workers complete to ensure all collected data is streamed
|
||||
go s.streamProgress(streamCtx, stopCtrl, streamCh, totalRequests, &counter, tuiLogChannel, showProgressBar)
|
||||
}
|
||||
|
||||
// Setup duration-based cancellation
|
||||
s.setupDurationTimeout(ctx, jobsCancel)
|
||||
s.setupDurationTimeout()
|
||||
// Distribute jobs to workers.
|
||||
// This blocks until all jobs are sent or the context is canceled.
|
||||
s.sendJobs(jobsCtx, jobsCh)
|
||||
s.sendJobs(jobsCh)
|
||||
|
||||
// Close the jobs channel so workers stop after completing their current job
|
||||
close(jobsCh)
|
||||
@@ -455,31 +461,31 @@ func (s sarin) startWorkers(wg *sync.WaitGroup, jobs <-chan struct{}, counter *a
|
||||
}
|
||||
}
|
||||
|
||||
func (s sarin) setupDurationTimeout(ctx context.Context, cancel context.CancelFunc) {
|
||||
func (s sarin) setupDurationTimeout() {
|
||||
if s.totalDuration != nil {
|
||||
go func() {
|
||||
timer := time.NewTimer(*s.totalDuration)
|
||||
defer timer.Stop()
|
||||
select {
|
||||
case <-timer.C:
|
||||
cancel()
|
||||
case <-ctx.Done():
|
||||
s.jobsCancel()
|
||||
case <-s.jobsCtx.Done():
|
||||
// Context cancelled, cleanup
|
||||
}
|
||||
}()
|
||||
}
|
||||
}
|
||||
|
||||
func (s sarin) sendJobs(ctx context.Context, jobs chan<- struct{}) {
|
||||
func (s sarin) sendJobs(jobs chan<- struct{}) {
|
||||
if s.totalRequests != nil && *s.totalRequests > 0 {
|
||||
for range *s.totalRequests {
|
||||
if ctx.Err() != nil {
|
||||
if s.jobsCtx.Err() != nil {
|
||||
break
|
||||
}
|
||||
jobs <- struct{}{}
|
||||
}
|
||||
} else {
|
||||
for ctx.Err() == nil {
|
||||
for s.jobsCtx.Err() == nil {
|
||||
jobs <- struct{}{}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -17,6 +17,7 @@ const scriptHTTPDefaultTimeout = 30 * time.Second
|
||||
|
||||
// scriptHTTPClient sends scripts' http.* requests. It is safe for concurrent use.
|
||||
type scriptHTTPClient struct {
|
||||
ctx context.Context //nolint:containedctx
|
||||
client *fasthttp.Client
|
||||
insecureClient *fasthttp.Client
|
||||
defaultTimeout time.Duration
|
||||
@@ -29,7 +30,7 @@ var _ script.HTTPDoer = (*scriptHTTPClient)(nil)
|
||||
// - types.ProxyDialError
|
||||
func newScriptHTTPClients(ctx context.Context, proxies []url.URL, maxConns uint) ([]*scriptHTTPClient, error) {
|
||||
if len(proxies) == 0 {
|
||||
return []*scriptHTTPClient{newScriptHTTPClient(fasthttp.DialDualStackTimeout, scriptHTTPDefaultTimeout, maxConns)}, nil
|
||||
return []*scriptHTTPClient{newScriptHTTPClient(ctx, fasthttp.DialDualStackTimeout, scriptHTTPDefaultTimeout, maxConns)}, nil
|
||||
}
|
||||
|
||||
clients := make([]*scriptHTTPClient, 0, len(proxies))
|
||||
@@ -38,12 +39,12 @@ func newScriptHTTPClients(ctx context.Context, proxies []url.URL, maxConns uint)
|
||||
if err != nil {
|
||||
return nil, types.NewProxyDialError(proxy.String(), err)
|
||||
}
|
||||
clients = append(clients, newScriptHTTPClient(dial, scriptHTTPDefaultTimeout, maxConns))
|
||||
clients = append(clients, newScriptHTTPClient(ctx, dial, scriptHTTPDefaultTimeout, maxConns))
|
||||
}
|
||||
return clients, nil
|
||||
}
|
||||
|
||||
func newScriptHTTPClient(dial fasthttp.DialFuncWithTimeout, defaultTimeout time.Duration, maxConns uint) *scriptHTTPClient {
|
||||
func newScriptHTTPClient(ctx context.Context, dial fasthttp.DialFuncWithTimeout, defaultTimeout time.Duration, maxConns uint) *scriptHTTPClient {
|
||||
newClient := func(insecure bool) *fasthttp.Client {
|
||||
// Read and write timeouts stay unset because they would cap the per-call timeout,
|
||||
// so dialWithDeadline is what bounds the TLS handshake.
|
||||
@@ -60,6 +61,7 @@ func newScriptHTTPClient(dial fasthttp.DialFuncWithTimeout, defaultTimeout time.
|
||||
}
|
||||
|
||||
return &scriptHTTPClient{
|
||||
ctx: ctx,
|
||||
client: newClient(false),
|
||||
insecureClient: newClient(true),
|
||||
defaultTimeout: defaultTimeout,
|
||||
@@ -91,6 +93,10 @@ func dialWithDeadline(dial fasthttp.DialFuncWithTimeout) fasthttp.DialFuncWithTi
|
||||
// It can return the following errors:
|
||||
// - types.ScriptHTTPRequestError
|
||||
func (c *scriptHTTPClient) Do(r *script.HTTPRequest) (*script.HTTPResponse, error) {
|
||||
if err := c.ctx.Err(); err != nil {
|
||||
return nil, types.NewScriptHTTPRequestError(r.Method, r.URL, err)
|
||||
}
|
||||
|
||||
parsedURL, err := url.Parse(r.URL)
|
||||
if err != nil {
|
||||
return nil, types.NewScriptHTTPRequestError(r.Method, r.URL, err)
|
||||
|
||||
+25
-11
@@ -2,6 +2,7 @@ package script
|
||||
|
||||
import (
|
||||
"errors"
|
||||
"strconv"
|
||||
|
||||
"github.com/dop251/goja"
|
||||
"go.aykhans.me/sarin/internal/types"
|
||||
@@ -195,22 +196,23 @@ func (e *JsEngine) objectToStringSliceMap(obj *goja.Object) map[string][]string
|
||||
result := make(map[string][]string)
|
||||
for _, key := range obj.Keys() {
|
||||
v := obj.Get(key)
|
||||
if v == nil || goja.IsUndefined(v) || goja.IsNull(v) {
|
||||
continue
|
||||
}
|
||||
|
||||
// Check if it's an array
|
||||
if arr, ok := v.Export().([]any); ok {
|
||||
var values []string
|
||||
for _, item := range arr {
|
||||
if s, ok := item.(string); ok {
|
||||
values = append(values, s)
|
||||
if arr, ok := v.(*goja.Object); ok && arr.ClassName() == "Array" {
|
||||
length := int(arr.Get("length").ToInteger())
|
||||
values := make([]string, 0, length)
|
||||
for i := range length {
|
||||
if text, ok := jsStringValue(arr.Get(strconv.Itoa(i))); ok {
|
||||
values = append(values, text)
|
||||
}
|
||||
}
|
||||
result[key] = values
|
||||
} else {
|
||||
// Single value, wrap it in a slice
|
||||
result[key] = []string{v.String()}
|
||||
continue
|
||||
}
|
||||
|
||||
// Single value, wrap it in a slice
|
||||
if text, ok := jsStringValue(v); ok {
|
||||
result[key] = []string{text}
|
||||
}
|
||||
}
|
||||
return result
|
||||
@@ -231,3 +233,15 @@ func (e *JsEngine) exceptionError(err error) error {
|
||||
}
|
||||
return errors.New(message)
|
||||
}
|
||||
|
||||
// jsStringValue renders a JavaScript primitive the way the script would see it.
|
||||
// Objects, arrays and functions have no useful text form, so they are skipped.
|
||||
func jsStringValue(v goja.Value) (string, bool) {
|
||||
if v == nil || goja.IsUndefined(v) || goja.IsNull(v) {
|
||||
return "", false
|
||||
}
|
||||
if _, isObject := v.(*goja.Object); isObject {
|
||||
return "", false
|
||||
}
|
||||
return v.String(), true
|
||||
}
|
||||
+24
-11
@@ -168,20 +168,33 @@ func (e *LuaEngine) tableToStringSliceMap(t *lua.LTable) map[string][]string {
|
||||
}
|
||||
key := string(k.(lua.LString))
|
||||
|
||||
switch v.Type() {
|
||||
case lua.LTString:
|
||||
// Single string value
|
||||
result[key] = []string{string(v.(lua.LString))}
|
||||
case lua.LTTable:
|
||||
// Array of strings
|
||||
var values []string
|
||||
v.(*lua.LTable).ForEach(func(_, item lua.LValue) {
|
||||
if item.Type() == lua.LTString {
|
||||
values = append(values, string(item.(lua.LString)))
|
||||
if v.Type() == lua.LTTable {
|
||||
// Array of values, keys outside 1..n are skipped
|
||||
arr := v.(*lua.LTable)
|
||||
length := arr.Len()
|
||||
values := make([]string, 0, length)
|
||||
for i := 1; i <= length; i++ {
|
||||
if text, ok := luaScalarString(arr.RawGetInt(i)); ok {
|
||||
values = append(values, text)
|
||||
}
|
||||
})
|
||||
}
|
||||
result[key] = values
|
||||
return
|
||||
}
|
||||
|
||||
if text, ok := luaScalarString(v); ok {
|
||||
result[key] = []string{text}
|
||||
}
|
||||
})
|
||||
return result
|
||||
}
|
||||
|
||||
// luaScalarString renders strings, numbers and booleans as text.
|
||||
func luaScalarString(v lua.LValue) (string, bool) {
|
||||
switch v.Type() {
|
||||
case lua.LTString, lua.LTNumber, lua.LTBool:
|
||||
return v.String(), true
|
||||
default:
|
||||
return "", false
|
||||
}
|
||||
}
|
||||
Reference in new issue
Block a user