Skip to content

Commit

Permalink
Merge pull request #22 from nakabonne/cli-flags
Browse files Browse the repository at this point in the history
Ensure to attack with cli options
  • Loading branch information
nakabonne authored Oct 1, 2020
2 parents f5474e7 + 2412484 commit 6900f3a
Show file tree
Hide file tree
Showing 15 changed files with 291 additions and 412 deletions.
32 changes: 17 additions & 15 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -55,24 +55,27 @@ docker run --rm -it nakabonne/ali ali
## Usage
### Quickstart

```bash
ali http://host.xz
```
$ ali
```

Click on the target URL input field, give the URL and press Enter. Then the attack will be launched with default options.
Replace `http://host.xz` with the target you want to issue the requests to.
Press Enter when the UI appears, then the attack will be launched with default options.

### Options

**Note** that UI field-based configuration is planned to eliminated and will only support configuration through CLI flags.
```bash
ali -h
```

| Name | Description | Default |
|------|-------------|---------|
| Rate Limit | The request rate per second to issue against the targets. Give 0 then it will send requests as fast as possible. | 50 |
| Duration | The amount of time to issue requests to the targets. Give `0s` for an infinite attack. Press `Ctrl-C` to stop. Valid time units are "ns", "us" (or "µs"), "ms", "s", "m", "h". | 10s |
| Timeout | The timeout for each request. `0s` means to disable timeouts. Valid time units are "ns", "us" (or "µs"), "ms", "s", "m", "h". | 30s |
| Method | An HTTP request method for each request. | GET |
| Header | A request header to be sent. | empty |
| Body | The file whose content will be set as the http request body. | empty |
| `--rate` | The request rate per second to issue against the targets. Give 0 then it will send requests as fast as possible. | 50 |
| `--duration` | The amount of time to issue requests to the targets. Give `0s` for an infinite attack. Press `Ctrl-C` to stop. Valid time units are "ns", "us" (or "µs"), "ms", "s", "m", "h". | 10s |
| `--timeout` | The timeout for each request. `0s` means to disable timeouts. Valid time units are "ns", "us" (or "µs"), "ms", "s", "m", "h". | 30s |
| `--method` | An HTTP request method for each request. | GET |
| `--header` | A request header to be sent. | |
| `--body` | A request body to be sent. | |
| `--body-file` | The path to file whose content will be set as the http request body. | |

## Features

Expand All @@ -99,7 +102,6 @@ With the help of [mum4k/termdash](https://github.com/mum4k/termdash), it's intui


## Roadmap
- Better UI
- Eliminate field-based configuration and only support configuration through cli flags
- Plot more metrics in real-time ([#2](https://github.com/nakabonne/ali/issues/2))
- Support more options for HTTP requests ([#1](https://github.com/nakabonne/ali/issues/1))
- [x] Eliminate field-based configuration and only support configuration through cli flags
- [ ] Support more options for HTTP requests ([#1](https://github.com/nakabonne/ali/issues/1))
- [ ] Plot more metrics in real-time ([#2](https://github.com/nakabonne/ali/issues/2))
4 changes: 3 additions & 1 deletion attacker/attacker.go
Original file line number Diff line number Diff line change
Expand Up @@ -49,7 +49,7 @@ type Result struct {
// Attack keeps the request running for the specified period of time.
// Results are sent to the given channel as soon as they arrive.
// When the attack is over, it gives back final statistics.
func Attack(ctx context.Context, target string, resCh chan *Result, opts Options) *Metrics {
func Attack(ctx context.Context, target string, resCh chan *Result, metricsCh chan *Metrics, opts Options) *Metrics {
if target == "" {
return nil
}
Expand Down Expand Up @@ -91,9 +91,11 @@ func Attack(ctx context.Context, target string, resCh chan *Result, opts Options
default:
resCh <- &Result{Latency: res.Latency}
metrics.Add(res)
metricsCh <- newMetrics(&metrics)
}
}
metrics.Close()

// TODO: No need to give back metrics anymore.
return newMetrics(&metrics)
}
3 changes: 2 additions & 1 deletion attacker/attacker_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -73,7 +73,8 @@ func TestAttack(t *testing.T) {
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
resCh := make(chan *Result, 100)
got := Attack(ctx, tt.target, resCh, tt.opts)
metricsCh := make(chan *Metrics, 100)
got := Attack(ctx, tt.target, resCh, metricsCh, tt.opts)
assert.Equal(t, tt.want, got)
assert.Equal(t, tt.wantResCount, len(resCh))
})
Expand Down
43 changes: 20 additions & 23 deletions gui/drawer.go
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,7 @@ type drawer struct {
chartCh chan *attacker.Result
gaugeCh chan bool
metricsCh chan *attacker.Metrics
// TODO: Remove
messageCh chan string

// aims to avoid to perform multiple `redrawChart`.
Expand Down Expand Up @@ -90,15 +91,15 @@ Out:
Total: %v
Mean: %v`

othersTextFormat = `Earliest: %v
Latest: %v
End: %v
Duration: %v
othersTextFormat = `Duration: %v
Wait: %v
Requests: %d
Rate: %f
Throughput: %f
Success: %f`
Success: %f
Earliest: %v
Latest: %v
End: %v`
)

func (d *drawer) redrawMetrics(ctx context.Context) {
Expand Down Expand Up @@ -131,35 +132,31 @@ func (d *drawer) redrawMetrics(ctx context.Context) {
metrics.BytesOut.Mean,
), text.WriteReplace())

othersText := fmt.Sprintf(othersTextFormat,
metrics.Earliest,
metrics.Latest,
metrics.End,
d.widgets.othersText.Write(fmt.Sprintf(othersTextFormat,
metrics.Duration,
metrics.Wait,
metrics.Requests,
metrics.Rate,
metrics.Throughput,
metrics.Success,
)
metrics.Earliest,
metrics.Latest,
metrics.End,
), text.WriteReplace())

if len(metrics.StatusCodes) > 0 {
othersText += `
StatusCodes:`
}
codesText := ""
for code, n := range metrics.StatusCodes {
othersText += fmt.Sprintf(`
%s: %d`, code, n)
}
if len(metrics.Errors) > 0 {
othersText += `
Errors:`
codesText += fmt.Sprintf(`%q: %d
`, code, n)
}
d.widgets.statusCodesText.Write(codesText, text.WriteReplace())

errorsText := ""
for i, e := range metrics.Errors {
othersText += fmt.Sprintf(`
%d: %s`, i, e)
errorsText += fmt.Sprintf(`%d: %s
`, i, e)
}
d.widgets.othersText.Write(othersText, text.WriteReplace())
d.widgets.errorsText.Write(errorsText, text.WriteReplace())
}
}
}
Expand Down
56 changes: 40 additions & 16 deletions gui/drawer_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -126,11 +126,13 @@ func TestRedrawMetrics(t *testing.T) {
defer ctrl.Finish()

tests := []struct {
name string
metrics *attacker.Metrics
latenciesText Text
bytesText Text
othersText Text
name string
metrics *attacker.Metrics
latenciesText Text
bytesText Text
othersText Text
statusCodesText Text
errorsText Text
}{
{
name: "nil metrics given",
Expand All @@ -146,6 +148,14 @@ func TestRedrawMetrics(t *testing.T) {
t := NewMockText(ctrl)
return t
}(),
statusCodesText: func() Text {
t := NewMockText(ctrl)
return t
}(),
errorsText: func() Text {
t := NewMockText(ctrl)
return t
}(),
},
{
name: "with errors",
Expand Down Expand Up @@ -192,6 +202,7 @@ Max: 1ns
Min: 1ns`, gomock.Any())
return t
}(),

bytesText: func() Text {
t := NewMockText(ctrl)
t.EXPECT().Write(`In:
Expand All @@ -202,21 +213,32 @@ Out:
Mean: 1`, gomock.Any())
return t
}(),

statusCodesText: func() Text {
t := NewMockText(ctrl)
t.EXPECT().Write(`"200": 2
`, gomock.Any())
return t
}(),

errorsText: func() Text {
t := NewMockText(ctrl)
t.EXPECT().Write(`0: error1
`, gomock.Any())
return t
}(),

othersText: func() Text {
t := NewMockText(ctrl)
t.EXPECT().Write(`Earliest: 2009-11-10 23:00:00 +0000 UTC
Latest: 2009-11-10 23:00:00 +0000 UTC
End: 2009-11-10 23:00:00 +0000 UTC
Duration: 1ns
t.EXPECT().Write(`Duration: 1ns
Wait: 1ns
Requests: 1
Rate: 1.000000
Throughput: 1.000000
Success: 1.000000
StatusCodes:
200: 2
Errors:
0: error1`, gomock.Any())
Earliest: 2009-11-10 23:00:00 +0000 UTC
Latest: 2009-11-10 23:00:00 +0000 UTC
End: 2009-11-10 23:00:00 +0000 UTC`, gomock.Any())

return t
}(),
Expand All @@ -228,9 +250,11 @@ Errors:
ctx, cancel := context.WithTimeout(context.Background(), 30*time.Second)
d := &drawer{
widgets: &widgets{
latenciesText: tt.latenciesText,
bytesText: tt.bytesText,
othersText: tt.othersText,
latenciesText: tt.latenciesText,
bytesText: tt.bytesText,
othersText: tt.othersText,
statusCodesText: tt.statusCodesText,
errorsText: tt.errorsText,
},
metricsCh: make(chan *attacker.Metrics),
}
Expand Down
43 changes: 16 additions & 27 deletions gui/gui.go
Original file line number Diff line number Diff line change
Expand Up @@ -23,16 +23,16 @@ const (

type runner func(ctx context.Context, t terminalapi.Terminal, c *container.Container, opts ...termdash.Option) error

func Run() error {
func Run(targetURL string, opts *attacker.Options) error {
t, err := termbox.New(termbox.ColorMode(terminalapi.ColorMode256))
if err != nil {
return fmt.Errorf("failed to generate terminal interface: %w", err)
}
defer t.Close()
return run(t, termdash.Run)
return run(t, termdash.Run, targetURL, opts)
}

func run(t *termbox.Terminal, r runner) error {
func run(t *termbox.Terminal, r runner, targetURL string, opts *attacker.Options) error {
ctx, cancel := context.WithCancel(context.Background())
defer cancel()

Expand All @@ -41,7 +41,7 @@ func run(t *termbox.Terminal, r runner) error {
return fmt.Errorf("failed to generate container: %w", err)
}

w, err := newWidgets()
w, err := newWidgets(targetURL, opts)
if err != nil {
return fmt.Errorf("failed to generate widgets: %w", err)
}
Expand All @@ -63,37 +63,26 @@ func run(t *termbox.Terminal, r runner) error {
go d.redrawMetrics(ctx)
go d.redrawMessage(ctx)

k := keybinds(ctx, cancel, d)
k := keybinds(ctx, cancel, d, targetURL, *opts)

return r(ctx, t, c, termdash.KeyboardSubscriber(k), termdash.RedrawInterval(redrawInterval))
}

func gridLayout(w *widgets) ([]container.Option, error) {
raw1 := grid.RowHeightPerc(65, grid.Widget(w.latencyChart, container.Border(linestyle.Light), container.BorderTitle("Latency (ms)")))
raw2 := grid.RowHeightPerc(30,
grid.ColWidthPerc(50,
grid.RowHeightPerc(34, grid.Widget(w.urlInput, container.Border(linestyle.Light), container.BorderTitle("Target URL"))),
grid.RowHeightPerc(33,
grid.ColWidthPerc(20, grid.Widget(w.rateLimitInput, container.Border(linestyle.Light), container.BorderTitle("Rate Limit"))),
grid.ColWidthPerc(20, grid.Widget(w.durationInput, container.Border(linestyle.Light), container.BorderTitle("Duration"))),
grid.ColWidthPerc(20, grid.Widget(w.timeoutInput, container.Border(linestyle.Light), container.BorderTitle("Timeout"))),
grid.ColWidthPerc(20, grid.Widget(w.methodInput, container.Border(linestyle.Light), container.BorderTitle("Method"))),
grid.ColWidthPerc(19, grid.Widget(w.headerInput, container.Border(linestyle.Light), container.BorderTitle("Header"))),
),
grid.RowHeightPerc(33, grid.Widget(w.bodyInput, container.Border(linestyle.Light), container.BorderTitle("Body"))),
),
grid.ColWidthPerc(50,
grid.RowHeightPerc(85,
grid.ColWidthPerc(25, grid.Widget(w.latenciesText, container.Border(linestyle.Light), container.BorderTitle("Latencies"))),
grid.ColWidthPerc(25, grid.Widget(w.bytesText, container.Border(linestyle.Light), container.BorderTitle("Bytes"))),
grid.ColWidthPerc(50, grid.Widget(w.othersText, container.Border(linestyle.Light), container.BorderTitle("Others"))),
),
grid.RowHeightPerc(15, grid.Widget(w.messageText, container.Border(linestyle.Light), container.BorderTitle("Message"))),
raw1 := grid.RowHeightPerc(70, grid.Widget(w.latencyChart, container.Border(linestyle.Light), container.BorderTitle("Latency (ms)")))
raw2 := grid.RowHeightPerc(25,
grid.ColWidthPerc(15, grid.Widget(w.paramsText, container.Border(linestyle.Light), container.BorderTitle("Parameters"))),
grid.ColWidthPerc(15, grid.Widget(w.latenciesText, container.Border(linestyle.Light), container.BorderTitle("Latencies"))),
grid.ColWidthPerc(15, grid.Widget(w.bytesText, container.Border(linestyle.Light), container.BorderTitle("Bytes"))),
grid.ColWidthPerc(15,
grid.RowHeightPerc(50, grid.Widget(w.statusCodesText, container.Border(linestyle.Light), container.BorderTitle("Status Codes"))),
grid.RowHeightPerc(50, grid.Widget(w.errorsText, container.Border(linestyle.Light), container.BorderTitle("Errors"))),
),
grid.ColWidthPerc(40, grid.Widget(w.othersText, container.Border(linestyle.Light), container.BorderTitle("Others"))),
)
raw3 := grid.RowHeightPerc(4,
grid.ColWidthPerc(50, grid.Widget(w.progressGauge, container.Border(linestyle.Light), container.BorderTitle("Progress"))),
grid.ColWidthPerc(50, grid.Widget(w.navi, container.Border(linestyle.Light))),
grid.ColWidthPerc(60, grid.Widget(w.progressGauge, container.Border(linestyle.Light), container.BorderTitle("Progress"))),
grid.ColWidthPerc(40, grid.Widget(w.navi, container.Border(linestyle.Light))),
)

builder := grid.New()
Expand Down
4 changes: 3 additions & 1 deletion gui/gui_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,8 @@ import (
"github.com/mum4k/termdash/terminal/terminalapi"
"github.com/stretchr/testify/assert"
"go.uber.org/goleak"

"github.com/nakabonne/ali/attacker"
)

func TestMain(m *testing.M) {
Expand Down Expand Up @@ -40,7 +42,7 @@ func TestRun(t *testing.T) {
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
err := run(&termbox.Terminal{}, tt.r)
err := run(&termbox.Terminal{}, tt.r, "", &attacker.Options{})
assert.Equal(t, tt.wantErr, err != nil)
})
}
Expand Down
Loading

0 comments on commit 6900f3a

Please sign in to comment.