diff --git a/README.md b/README.md index 4c1890de..0323dc8f 100644 --- a/README.md +++ b/README.md @@ -47,6 +47,10 @@ attack command: Max open idle connections per target host (default 10000) -duration duration Duration of the test [0 = forever] + -format string + Targets format [legacy] (default "legacy") + -h2c + Send HTTP/2 requests without TLS encryption -header value Request header -http2 @@ -113,47 +117,7 @@ Specifies which profiler to enable during execution. Both *cpu* and #### `-version` Prints the version and exits. -### `attack` -```console -$ vegeta attack -h -Usage of vegeta attack: - -body string - Requests body file - -cert string - TLS client PEM encoded certificate file - -connections int - Max open idle connections per target host (default 10000) - -duration duration - Duration of the test [0 = forever] - -header value - Request header - -http2 - Send HTTP/2 requests when supported by the server (default true) - -insecure - Ignore invalid server TLS certificates - -keepalive - Use persistent connections (default true) - -key string - TLS client PEM encoded private key file - -laddr value - Local IP address (default 0.0.0.0) - -lazy - Read targets lazily - -output string - Output file (default "stdout") - -rate uint - Requests per second (default 50) - -redirects int - Number of redirects to follow. -1 will not follow but marks as success (default 10) - -root-certs value - TLS root certificate files (comma separated list) - -targets string - Targets file (default "stdin") - -timeout duration - Requests timeout (default 30s) - -workers uint - Initial number of workers (default 10) -``` +### `attack` command #### `-body` Specifies the file whose content will be set as the body of every @@ -172,6 +136,56 @@ The internal concurrency structure's setup has this value as a variable. The actual run time of the test can be longer than specified due to the responses delay. Use 0 for an infinite attack. +#### `-format` +Specifies the targets format to decode. + +##### `legacy` format + +The ill-defined legacy format almost resembles the plain-text HTTP message format but +doesn't support in-line HTTP bodies, only references to files that are loaded and used +as request bodies (as exemplified below). + +Although targets in this format can be produced by other programs, it was originally +meant to be used by people writing targets by hand for simple use cases. + +Here are a few examples of valid targets files in the legacy format: + +Simple targets +``` +GET http://goku:9090/path/to/dragon?item=ball +GET http://user:password@goku:9090/path/to +HEAD http://goku:9090/path/to/success +``` + +Targets with custom headers +``` +GET http://user:password@goku:9090/path/to +X-Account-ID: 8675309 + +DELETE http://goku:9090/path/to/remove +Confirmation-Token: 90215 +Authorization: Token DEADBEEF +``` + +Targets with custom bodies +``` +POST http://goku:9090/things +@/path/to/newthing.json + +PATCH http://goku:9090/thing/71988591 +@/path/to/thing-71988591.json +``` + +Targets with custom bodies and headers +``` +POST http://goku:9090/things +X-Account-ID: 99 +@/path/to/newthing.json +``` + +#### `-h2c` +Specifies that HTTP2 requests are to be sent over TCP without TLS encryption. + #### `-header` Specifies a request header to be used in all targets defined, see `-targets`. You can specify as many as needed by repeating the flag. @@ -220,39 +234,6 @@ list. If unspecified, the default system CAs certificates will be used. Specifies the attack targets in a line separated file, defaulting to stdin. The format should be as follows, combining any or all of the following: -Simple targets -``` -GET http://goku:9090/path/to/dragon?item=balls -GET http://user:password@goku:9090/path/to -HEAD http://goku:9090/path/to/success -``` - -Targets with custom headers -``` -GET http://user:password@goku:9090/path/to -X-Account-ID: 8675309 - -DELETE http://goku:9090/path/to/remove -Confirmation-Token: 90215 -Authorization: Token DEADBEEF -``` - -Targets with custom bodies -``` -POST http://goku:9090/things -@/path/to/newthing.json - -PATCH http://goku:9090/thing/71988591 -@/path/to/thing-71988591.json -``` - -Targets with custom bodies and headers -``` -POST http://goku:9090/things -X-Account-ID: 99 -@/path/to/newthing.json -``` - #### `-timeout` Specifies the timeout for each request. The default is 0 which disables timeouts. @@ -262,17 +243,7 @@ Specifies the initial number of workers used in the attack. The actual number of workers will increase if necessary in order to sustain the requested rate. -### report -```console -$ vegeta report -h -Usage of vegeta report: - -inputs string - Input files (comma separated) (default "stdin") - -output string - Output file (default "stdout") - -reporter string - Reporter [text, json, plot, hist[buckets]] (default "text") -``` +### report command #### `-inputs` Specifies the input files to generate the report of, defaulting to stdin. @@ -363,17 +334,7 @@ Bucket # % Histogram [6ms, +Inf] 4771 25.93% ################### ``` -### `dump` -```console -$ vegeta dump -h -Usage of vegeta dump: - -dumper string - Dumper [json, csv] (default "json") - -inputs string - Input files (comma separated) (default "stdin") - -output string - Output file (default "stdout") -``` +### `dump` command #### `-inputs` Specifies the input files containing attack results to be dumped. You can specify more than one (comma separated). diff --git a/attack.go b/attack.go index 2c017595..9c9ce3cc 100644 --- a/attack.go +++ b/attack.go @@ -25,6 +25,7 @@ func attackCmd() command { fs.StringVar(&opts.name, "name", "", "Attack name") fs.StringVar(&opts.targetsf, "targets", "stdin", "Targets file") + fs.StringVar(&opts.format, "format", "legacy", "Targets format [legacy]") fs.StringVar(&opts.outputf, "output", "stdout", "Output file") fs.StringVar(&opts.bodyf, "body", "", "Requests body file") fs.StringVar(&opts.certf, "cert", "", "TLS client PEM encoded certificate file") @@ -59,6 +60,7 @@ var ( type attackOpts struct { name string targetsf string + format string outputf string bodyf string certf string @@ -111,10 +113,16 @@ func attack(opts *attackOpts) (err error) { src = files[opts.targetsf] hdr = opts.headers.Header ) - if opts.lazy { - tr = vegeta.NewLazyTargeter(src, body, hdr) - } else if tr, err = vegeta.NewEagerTargeter(src, body, hdr); err != nil { - return err + + switch opts.format { + case "legacy": + tr = vegeta.NewLegacyTargeter(src, body, hdr) + } + + if !opts.lazy { + if tr, err = vegeta.NewEagerTargeter(tr); err != nil { + return err + } } out, err := file(opts.outputf, true) diff --git a/lib/targets.go b/lib/targets.go index 9624aa2f..b27820a8 100644 --- a/lib/targets.go +++ b/lib/targets.go @@ -64,38 +64,45 @@ func NewStaticTargeter(tgts ...Target) Targeter { } } -// NewEagerTargeter eagerly reads all Targets out of the provided io.Reader and +// NewEagerTargeter eagerly reads all Targets out of the provided Targeter and // returns a NewStaticTargeter with them. -// -// body will be set as the Target's body if no body is provided. -// hdr will be merged with the each Target's headers. -func NewEagerTargeter(src io.Reader, body []byte, header http.Header) (Targeter, error) { +func NewEagerTargeter(t Targeter) (Targeter, error) { var ( - sc = NewLazyTargeter(src, body, header) tgts []Target tgt Target err error ) + for { - if err = sc(&tgt); err == ErrNoTargets { + if err = t(&tgt); err == ErrNoTargets { break } else if err != nil { return nil, err } tgts = append(tgts, tgt) } + if len(tgts) == 0 { return nil, ErrNoTargets } + return NewStaticTargeter(tgts...), nil } -// NewLazyTargeter returns a new Targeter that lazily scans Targets from the -// provided io.Reader on every invocation. +// NewLegacyTargeter returns a new Targeter that decodes one Target from the +// given io.Reader on every invocation. The format is as follows: +// +// GET https://foo.bar/a/b/c +// Header-X: 123 +// Header-Y: 321 +// @/path/to/body/file +// +// POST https://foo.bar/b/c/a +// Header-X: 123 // // body will be set as the Target's body if no body is provided. // hdr will be merged with the each Target's headers. -func NewLazyTargeter(src io.Reader, body []byte, hdr http.Header) Targeter { +func NewLegacyTargeter(src io.Reader, body []byte, hdr http.Header) Targeter { var mu sync.Mutex sc := peekingScanner{src: bufio.NewScanner(src)} return func(tgt *Target) (err error) { diff --git a/lib/targets_test.go b/lib/targets_test.go index c645661f..00730122 100644 --- a/lib/targets_test.go +++ b/lib/targets_test.go @@ -62,7 +62,7 @@ func TestNewEagerTargeter(t *testing.T) { t.Parallel() src := []byte("GET http://:6060/\nHEAD http://:6606/") - read, err := NewEagerTargeter(bytes.NewReader(src), []byte("body"), nil) + read, err := NewEagerTargeter(NewLegacyTargeter(bytes.NewReader(src), []byte("body"), nil)) if err != nil { t.Fatalf("Couldn't parse valid source: %s", err) } @@ -89,7 +89,7 @@ func TestNewEagerTargeter(t *testing.T) { } } -func TestNewLazyTargeter(t *testing.T) { +func TestNewLegacyTargeter(t *testing.T) { t.Parallel() for want, def := range map[error]string{ @@ -111,7 +111,7 @@ func TestNewLazyTargeter(t *testing.T) { : 1234`, } { src := bytes.NewBufferString(strings.TrimSpace(def)) - read := NewLazyTargeter(src, []byte{}, http.Header{}) + read := NewLegacyTargeter(src, []byte{}, http.Header{}) if got := read(&Target{}); got == nil || !strings.HasPrefix(got.Error(), want.Error()) { t.Errorf("got: %s, want: %s\n%s", got, want, def) } @@ -149,7 +149,7 @@ func TestNewLazyTargeter(t *testing.T) { ) src := bytes.NewBufferString(strings.TrimSpace(targets)) - read := NewLazyTargeter(src, []byte{}, http.Header{"Content-Type": []string{"text/plain"}}) + read := NewLegacyTargeter(src, []byte{}, http.Header{"Content-Type": []string{"text/plain"}}) for _, want := range []Target{ { Method: "GET", @@ -215,13 +215,13 @@ func TestNewLazyTargeter(t *testing.T) { func TestErrNilTarget(t *testing.T) { t.Parallel() - eager, err := NewEagerTargeter(strings.NewReader("GET http://foo.bar"), nil, nil) + eager, err := NewEagerTargeter(NewLegacyTargeter(strings.NewReader("GET http://foo.bar"), nil, nil)) if err != nil { t.Fatal(err) } for i, tr := range []Targeter{ NewStaticTargeter(Target{Method: "GET", URL: "http://foo.bar"}), - NewLazyTargeter(strings.NewReader("GET http://foo.bar"), nil, nil), + NewLegacyTargeter(strings.NewReader("GET http://foo.bar"), nil, nil), eager, } { if got, want := tr(nil), ErrNilTarget; got != want {