mirror of
https://github.com/aykhans/sarin.git
synced 2026-10-12 20:58:52 +00:00
Compare commits
3
Commits
9e773a4e6c
...
bea48eeab0
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
bea48eeab0
|
||
|
|
cd0fa453c2
|
||
|
|
847c4b39ce
|
No files matched your search
@@ -181,6 +181,8 @@ A server can declare a `Content-Length` far larger than it sends, and the body i
|
||||
|
||||
The limit applies per response in flight, and a declared Content-Length is allocated up to it, so the worst case is a few times the limit times the concurrency. It covers the requests scripts make as well.
|
||||
|
||||
A compressed response in a script is decoded first, so the limit covers the decoded size as well as the bytes that arrived. Decoding holds both at once, so a brotli or zstd body peaks at over ten times the limit. Ratios are unbounded, and 106 bytes of brotli can hold 64MiB, so with `0` a tiny download can still exhaust memory.
|
||||
|
||||
## Concurrency
|
||||
|
||||
Number of concurrent workers. Must be between 1 and 100,000,000. Defaults to `1`.
|
||||
@@ -232,6 +234,8 @@ Using `none` disables output and reduces memory usage since response statistics
|
||||
|
||||
Generate requests without sending them. Useful for testing templates.
|
||||
|
||||
A script's own `http` calls are still sent, since a script needs them to build the request you are inspecting. Only the requests sarin generates are held back.
|
||||
|
||||
## Insecure
|
||||
|
||||
Skip TLS certificate verification.
|
||||
@@ -622,6 +626,7 @@ In Lua, call the helpers as `res:header("name")` or `res.header("name")`.
|
||||
- **Status codes:** non-2xx responses are returned normally; check `status` in the script.
|
||||
- **Errors:** network failures and timeouts raise a script error. The main request is not sent and the error is counted in the results. Scripts can catch it with `pcall` (Lua) or `try`/`catch` (JavaScript).
|
||||
- **Response bodies:** the [Max Response Body](#max-response-body) config bounds these requests too, and a body above it raises an error the script can catch.
|
||||
- **Compressed responses:** sarin asks for no encoding, so this only applies when a script sends `Accept-Encoding` itself or the server compresses unasked. A `gzip`, `deflate`, `br` or `zstd` body is then decoded before the script sees it, and `Content-Encoding` and `Content-Length` are both dropped from `headers` because neither describes what the script receives. Anything else is left raw with its header intact: an empty body, `identity`, a `Content-Encoding` sarin does not recognise, and two or more codings at once. A body that fails to decode raises an error the script can catch, though `br` carries no checksum, so damage to a brotli body can go unnoticed.
|
||||
- **Only inside `transform`:** `http` works anywhere while `transform` runs, including in helper functions it calls. Calling it at the top level of a script raises an error, because that code also runs during config validation and when each worker starts.
|
||||
- **Dry run:** script requests are still sent in dry-run mode.
|
||||
- **Not measured:** script requests are not included in the results.
|
||||
|
||||
@@ -31,7 +31,7 @@ Flags:
|
||||
-w, -log-file string Write runtime logs to this file instead of the terminal/stderr
|
||||
-p, -progress string Progress display (possible values: bar, none) (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)
|
||||
-z, -dry-run bool Run without sending the generated requests (default %v)
|
||||
|
||||
Request Config:
|
||||
-U, -url string Target URL for the request
|
||||
@@ -145,8 +145,8 @@ func (parser ConfigCLIParser) Parse() (*Config, error) {
|
||||
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")
|
||||
flagSet.BoolVar(&dryRun, "dry-run", false, "Run without sending the generated requests")
|
||||
flagSet.BoolVar(&dryRun, "z", false, "Run without sending the generated requests")
|
||||
|
||||
// Request config
|
||||
flagSet.StringVar(&urlInput, "url", "", "Target URL for the request")
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
package sarin
|
||||
|
||||
import (
|
||||
"compress/zlib"
|
||||
"context"
|
||||
"crypto/tls"
|
||||
"errors"
|
||||
@@ -18,10 +19,11 @@ 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
|
||||
ctx context.Context //nolint:containedctx
|
||||
client *fasthttp.Client
|
||||
insecureClient *fasthttp.Client
|
||||
defaultTimeout time.Duration
|
||||
maxResponseBody int
|
||||
}
|
||||
|
||||
var _ script.HTTPDoer = (*scriptHTTPClient)(nil)
|
||||
@@ -71,10 +73,11 @@ func newScriptHTTPClient(
|
||||
}
|
||||
|
||||
return &scriptHTTPClient{
|
||||
ctx: ctx,
|
||||
client: newClient(false),
|
||||
insecureClient: newClient(true),
|
||||
defaultTimeout: defaultTimeout,
|
||||
ctx: ctx,
|
||||
client: newClient(false),
|
||||
insecureClient: newClient(true),
|
||||
defaultTimeout: defaultTimeout,
|
||||
maxResponseBody: safeUint64ToInt(maxResponseBody),
|
||||
}
|
||||
}
|
||||
|
||||
@@ -166,7 +169,10 @@ func (c *scriptHTTPClient) Do(r *script.HTTPRequest) (*script.HTTPResponse, erro
|
||||
return nil, types.NewScriptHTTPRequestError(r.Method, r.URL, err)
|
||||
}
|
||||
|
||||
body := resp.Body()
|
||||
body, err := decodeResponseBody(resp, c.maxResponseBody)
|
||||
if err != nil {
|
||||
return nil, types.NewScriptHTTPRequestError(r.Method, r.URL, err)
|
||||
}
|
||||
|
||||
return &script.HTTPResponse{
|
||||
Status: resp.StatusCode(),
|
||||
@@ -174,3 +180,31 @@ func (c *scriptHTTPClient) Do(r *script.HTTPRequest) (*script.HTTPResponse, erro
|
||||
Body: string(body),
|
||||
}, nil
|
||||
}
|
||||
|
||||
// decodeResponseBody decodes a body the server compressed, since scripts read it as text.
|
||||
// The limit covers the decoded size, which is what a compression bomb inflates to.
|
||||
// An empty body is left alone because the decoders reject zero bytes, and a HEAD or a 304
|
||||
// carries the encoding with no body. A coding fasthttp does not decode, and the bare
|
||||
// deflate stream some servers send without a zlib header, are left raw for the script.
|
||||
func decodeResponseBody(resp *fasthttp.Response, maxResponseBody int) ([]byte, error) {
|
||||
if len(resp.Header.ContentEncoding()) == 0 || len(resp.Body()) == 0 {
|
||||
return resp.Body(), nil
|
||||
}
|
||||
|
||||
decoded, err := resp.BodyUncompressedWithLimit(maxResponseBody)
|
||||
if errors.Is(err, fasthttp.ErrContentEncodingUnsupported) || errors.Is(err, zlib.ErrHeader) {
|
||||
return resp.Body(), nil
|
||||
}
|
||||
if err != nil {
|
||||
if errors.Is(err, fasthttp.ErrBodyTooLarge) {
|
||||
err = types.ErrResponseBodyTooLarge
|
||||
}
|
||||
return nil, err
|
||||
}
|
||||
|
||||
// Neither header describes the decoded body, so both go, as net/http does on a gunzip.
|
||||
resp.Header.SetContentEncoding("")
|
||||
resp.Header.Del(fasthttp.HeaderContentLength)
|
||||
|
||||
return decoded, nil
|
||||
}
|
||||
@@ -45,9 +45,14 @@ func luaJSONDecode(state *lua.LState) int {
|
||||
return 1
|
||||
}
|
||||
|
||||
// luaJSONMaxDepth matches the depth encoding/json accepts, so a table a script
|
||||
// decoded always encodes again, and it stays far below what overflows the Go stack.
|
||||
const luaJSONMaxDepth = 10000
|
||||
|
||||
// luaToGo converts a Lua value to a JSON-encodable Go value.
|
||||
// It can return the following errors:
|
||||
// - types.ErrScriptJSONCycle
|
||||
// - types.ErrScriptJSONDepth
|
||||
// - types.ScriptTypeError
|
||||
func luaToGo(value lua.LValue, visiting map[*lua.LTable]bool) (any, error) {
|
||||
switch v := value.(type) {
|
||||
@@ -63,6 +68,12 @@ func luaToGo(value lua.LValue, visiting map[*lua.LTable]bool) (any, error) {
|
||||
if visiting[v] {
|
||||
return nil, types.ErrScriptJSONCycle
|
||||
}
|
||||
// visiting holds the tables on the current path, so its size is the depth.
|
||||
// A table millions of levels deep overflows the Go stack, which is a fatal
|
||||
// error no pcall can catch, so the descent is bounded well before that.
|
||||
if len(visiting) >= luaJSONMaxDepth {
|
||||
return nil, types.ErrScriptJSONDepth
|
||||
}
|
||||
visiting[v] = true
|
||||
defer delete(visiting, v)
|
||||
|
||||
|
||||
@@ -442,6 +442,7 @@ var (
|
||||
ErrScriptHTTPURLInvalid = errors.New("URL must be an absolute http or https URL")
|
||||
ErrScriptHTTPUnknownOption = errors.New("unknown option")
|
||||
ErrScriptJSONCycle = errors.New("cannot encode a table that contains itself")
|
||||
ErrScriptJSONDepth = errors.New("cannot encode a table nested too deeply")
|
||||
)
|
||||
|
||||
type ScriptTypeError struct {
|
||||
|
||||
Reference in new issue
Block a user