summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
authorShulhan <ms@kilabit.info>2021-03-21 18:34:30 +0700
committerShulhan <ms@kilabit.info>2021-03-21 18:34:30 +0700
commit5828835bcd18eff070789fd82401480c244bbc27 (patch)
tree0b030e1f037684a9f5e81d62d8db52dd6ce69532
parentab00b51a1353c7f17697e027061b4c20509a7a80 (diff)
downloadgorankusu-5828835bcd18eff070789fd82401480c244bbc27.tar.xz
all: load pass attack results and implement function to get attack result
When the service started, it will load all previous attack results from directory Environment.ResultsDir. It will only scan the file name and append it to HttpTarget.Results due to the size and time to load one of them can take time. Through the web interface, user can click "Show" button to load the result and display it on the screen.
-rw-r--r--.gitignore2
-rw-r--r--_www/index.js47
-rw-r--r--attack_result.go30
-rw-r--r--errors.go11
-rw-r--r--example/example.go4
-rw-r--r--http_target.go23
-rw-r--r--trunks.go145
7 files changed, 214 insertions, 48 deletions
diff --git a/.gitignore b/.gitignore
index d2a9400..3323914 100644
--- a/.gitignore
+++ b/.gitignore
@@ -1,2 +1,2 @@
/trunks
-/testdata/example
+/example/testdata
diff --git a/_www/index.js b/_www/index.js
index 0efc88a..642a7bc 100644
--- a/_www/index.js
+++ b/_www/index.js
@@ -165,7 +165,6 @@ function renderTarget(targetID) {
}
w += "</div>"
-
}
w += "</div>"
@@ -211,14 +210,16 @@ function renderHttpAttackResults(target, http) {
for (let x = 0; x < http.Results.length; x++) {
let result = http.Results[x]
w += `
- <div class="result">
- <div>${result.Name}</div>
- <pre class="mono">
-${atob(result.TextReport)}
- </pre>
- <pre class="mono">
-${atob(result.HistReport)}
- </pre>
+ <div>
+ <button onclick="getAttackResult(this, '${result.Name}')">
+ Show
+ </button>
+ &nbsp;
+ --
+ &nbsp;
+ ${result.Name}
+ </div>
+ <div class="result" id="${result.Name}" style="display: none;">
</div>
`
}
@@ -226,7 +227,6 @@ ${atob(result.HistReport)}
return w
}
-
async function run(targetID, httpTargetID) {
let req = {}
req.Target = _targets[targetID]
@@ -273,6 +273,33 @@ async function attack(targetID, httpTargetID) {
let res = await fres.json()
}
+async function getAttackResult(button, name) {
+ let el = document.getElementById(name)
+
+ if (el.style.display === "block") {
+ el.style.display = "none"
+ button.innerHTML = "Show"
+ return
+ }
+
+ let url = "/_trunks/api/target/attack/result?name=" + name
+ let fres = await fetch(url)
+ let res = await fres.json()
+ let result = res.data
+
+ el.innerHTML = `
+ <pre class="mono">
+${atob(result.TextReport)}
+ </pre>
+ <pre class="mono">
+${atob(result.HistReport)}
+ </pre>
+ `
+
+ el.style.display = "block"
+ button.innerHTML = "Hide"
+}
+
function getHttpTargetByID(target, id) {
for (let x = 0; x < target.HttpTargets.length; x++) {
if (id == target.HttpTargets[x].ID) {
diff --git a/attack_result.go b/attack_result.go
index 1f93126..112f2ca 100644
--- a/attack_result.go
+++ b/attack_result.go
@@ -33,10 +33,10 @@ const (
type AttackResult struct {
mtx sync.Mutex
- TargetID string // TargetID the ID of HTTP target which own the result.
- Name string // Name of output file without path.
- TextReport []byte // TextReport the result reported as text.
- HistReport []byte // HistReport the result reported as histogram text.
+ HttpTargetID string // ID of HTTP target which own the result.
+ Name string // Name of output file without path.
+ TextReport []byte // TextReport the result reported as text.
+ HistReport []byte // HistReport the result reported as histogram text.
fullpath string
fout *os.File
@@ -52,9 +52,9 @@ func newAttackResult(env *Environment, rr *RunRequest) (
ar *AttackResult, err error,
) {
ar = &AttackResult{
- TargetID: rr.HttpTarget.ID,
- metrics: &vegeta.Metrics{},
- hist: &vegeta.Histogram{},
+ HttpTargetID: rr.HttpTarget.ID,
+ metrics: &vegeta.Metrics{},
+ hist: &vegeta.Histogram{},
}
ar.Name = fmt.Sprintf("%s.%s.%s.%dx%s.%s.bin",
@@ -102,14 +102,14 @@ func (ar *AttackResult) cancel() {
if ar.fout != nil {
err := ar.fout.Close()
if err != nil {
- mlog.Errf("AttackResult.cancel %s: %s\n", ar.TargetID, err)
+ mlog.Errf("AttackResult.cancel %s: %s\n", ar.HttpTargetID, err)
}
ar.fout = nil
if len(ar.fullpath) > 0 {
err = os.Remove(ar.fullpath)
if err != nil {
- mlog.Errf("AttackResult.cancel %s: %s\n", ar.TargetID, err)
+ mlog.Errf("AttackResult.cancel %s: %s\n", ar.HttpTargetID, err)
}
}
}
@@ -129,7 +129,7 @@ func (ar *AttackResult) finish() (err error) {
if ar.fout != nil {
err = ar.fout.Close()
if err != nil {
- return fmt.Errorf("%s: %w", ar.TargetID, err)
+ return fmt.Errorf("%s: %w", ar.HttpTargetID, err)
}
ar.fout = nil
}
@@ -157,19 +157,21 @@ func (ar *AttackResult) finish() (err error) {
return nil
}
-func (ar *AttackResult) init(path string) (err error) {
- ar.fullpath = filepath.Join(path, ar.Name)
+func (ar *AttackResult) load() (err error) {
+ if ar.TextReport != nil && ar.HistReport != nil {
+ return nil
+ }
result, err := ioutil.ReadFile(ar.fullpath)
if err != nil {
return err
}
- dec := vegeta.NewDecoder(bytes.NewReader(result))
-
ar.metrics = &vegeta.Metrics{}
ar.hist = &vegeta.Histogram{}
+ dec := vegeta.NewDecoder(bytes.NewReader(result))
+
err = ar.hist.Buckets.UnmarshalText([]byte(histogramBuckets))
if err != nil {
return err
diff --git a/errors.go b/errors.go
index 6351ab4..29dcfee 100644
--- a/errors.go
+++ b/errors.go
@@ -46,6 +46,17 @@ func errInternal(err error) error {
return res
}
+func errInvalidParameter(key, value string) error {
+ res := &libhttp.EndpointResponse{
+ E: liberrors.E{
+ Code: http.StatusBadRequest,
+ Message: fmt.Sprintf("invalid or emtpy parameter %q: %q", key, value),
+ Name: "ERR_INVALID_PARAMETER",
+ },
+ }
+ return res
+}
+
func errInvalidTarget(id string) error {
res := &libhttp.EndpointResponse{
E: liberrors.E{
diff --git a/example/example.go b/example/example.go
index c49400e..190b39a 100644
--- a/example/example.go
+++ b/example/example.go
@@ -32,7 +32,7 @@ type Example struct {
//
func New() (ex *Example, err error) {
env := &trunks.Environment{
- ResultsDir: "testdata/example/",
+ ResultsDir: "example/testdata/",
ResultsSuffix: "example",
}
@@ -92,7 +92,7 @@ func (ex *Example) registerEndpoints() (err error) {
func (ex *Example) registerTargets() (err error) {
targetHttp := &trunks.Target{
- Name: "Example HTTP target",
+ Name: "Example",
Opts: &trunks.AttackOptions{
BaseUrl: fmt.Sprintf("http://%s", ex.trunks.Env.ListenAddress),
Duration: 5 * time.Second,
diff --git a/http_target.go b/http_target.go
index 24603ca..03c05b1 100644
--- a/http_target.go
+++ b/http_target.go
@@ -5,7 +5,7 @@
package trunks
import (
- "fmt"
+ "path/filepath"
"sync"
libhttp "github.com/shuLhan/share/lib/http"
@@ -89,18 +89,23 @@ func (ht *HttpTarget) deleteResult(result *AttackResult) {
ht.Results = ht.Results[:len(ht.Results)-1]
}
-func (ht *HttpTarget) addResult(path, name string) (err error) {
+func (ht *HttpTarget) addResult(dir, name string) (err error) {
ar := &AttackResult{
- TargetID: ht.ID,
- Name: name,
- }
-
- err = ar.init(path)
- if err != nil {
- return fmt.Errorf("HttpTarget.addResult: %s %s: %w", path, name, err)
+ HttpTargetID: ht.ID,
+ Name: name,
+ fullpath: filepath.Join(dir, name),
}
ht.Results = append(ht.Results, ar)
return nil
}
+
+func (ht *HttpTarget) getResultByName(name string) (result *AttackResult) {
+ for _, result = range ht.Results {
+ if result.Name == name {
+ return result
+ }
+ }
+ return nil
+}
diff --git a/trunks.go b/trunks.go
index aadb66c..df6f2c0 100644
--- a/trunks.go
+++ b/trunks.go
@@ -8,12 +8,15 @@ import (
"encoding/json"
"fmt"
"net/http"
+ "os"
"sort"
+ "strings"
"time"
vegeta "github.com/tsenart/vegeta/v12/lib"
"github.com/shuLhan/share/lib/debug"
+ liberrors "github.com/shuLhan/share/lib/errors"
libhttp "github.com/shuLhan/share/lib/http"
"github.com/shuLhan/share/lib/memfs"
"github.com/shuLhan/share/lib/mlog"
@@ -30,11 +33,16 @@ const (
// List of HTTP APIs provided by Trunks HTTP server.
const (
- apiEnvironment = "/_trunks/api/environment"
- apiTargetAttack = "/_trunks/api/target/attack"
- apiTargetAttackResults = "/_trunks/api/target/attack/results"
- apiTargetRun = "/_trunks/api/target/run"
- apiTargets = "/_trunks/api/targets"
+ apiEnvironment = "/_trunks/api/environment"
+ apiTargetAttack = "/_trunks/api/target/attack"
+ apiTargetAttackResult = "/_trunks/api/target/attack/result"
+ apiTargetRun = "/_trunks/api/target/run"
+ apiTargets = "/_trunks/api/targets"
+)
+
+// List of HTTP parameters.
+const (
+ paramNameName = "name"
)
//
@@ -108,10 +116,13 @@ func (trunks *Trunks) RegisterTarget(target *Target) (err error) {
// load testing registered Targets.
//
func (trunks *Trunks) Start() (err error) {
- mlog.Outf("Starting attack worker...\n")
+ mlog.Outf("trunks: scanning previous attack results...\n")
+ trunks.scanAttackResults()
+
+ mlog.Outf("trunks: starting attack worker...\n")
go trunks.workerAttackQueue()
- mlog.Outf("starting HTTP server at %s\n", trunks.Env.ListenAddress)
+ mlog.Outf("trunks: starting HTTP server at %s\n", trunks.Env.ListenAddress)
return trunks.Server.Start()
}
@@ -176,17 +187,17 @@ func (trunks *Trunks) registerHttpApis() (err error) {
err = trunks.Server.RegisterEndpoint(&libhttp.Endpoint{
Method: libhttp.RequestMethodGet,
- Path: apiTargetAttackResults,
+ Path: apiTargetAttackResult,
RequestType: libhttp.RequestTypeQuery,
ResponseType: libhttp.ResponseTypeJSON,
- Call: trunks.apiTargetAttackResultsGet,
+ Call: trunks.apiTargetAttackResultGet,
})
if err != nil {
return err
}
err = trunks.Server.RegisterEndpoint(&libhttp.Endpoint{
Method: libhttp.RequestMethodDelete,
- Path: apiTargetAttackResults,
+ Path: apiTargetAttackResult,
RequestType: libhttp.RequestTypeJSON,
ResponseType: libhttp.ResponseTypeJSON,
Call: trunks.apiTargetAttackResultsDelete,
@@ -290,8 +301,45 @@ func (trunks *Trunks) apiTargetAttackCancel(epr *libhttp.EndpointRequest) (resbo
return resbody, nil
}
-func (trunks *Trunks) apiTargetAttackResultsGet(epr *libhttp.EndpointRequest) (resbody []byte, err error) {
- return resbody, nil
+func (trunks *Trunks) apiTargetAttackResultGet(epr *libhttp.EndpointRequest) (resbody []byte, err error) {
+ res := &libhttp.EndpointResponse{
+ E: liberrors.E{
+ Code: http.StatusNotFound,
+ Name: "ERR_ATTACK_RESULT_NOT_FOUND",
+ },
+ }
+
+ name := epr.HttpRequest.Form.Get(paramNameName)
+ if len(name) == 0 {
+ return nil, errInvalidParameter(paramNameName, name)
+ }
+
+ t, ht := trunks.getTargetByResultFilename(name)
+ if t == nil {
+ res.Message = "Target ID not found"
+ return nil, res
+ }
+ if ht == nil {
+ res.Message = "HttpTarget ID not found"
+ return nil, res
+ }
+
+ result := ht.getResultByName(name)
+ if result == nil {
+ res.Message = "Result file not found"
+ return nil, res
+ }
+
+ err = result.load()
+ if err != nil {
+ return nil, err
+ }
+
+ res.Code = http.StatusOK
+ res.Name = "OK_TARGET_ATTACK_RESULT"
+ res.Data = result
+
+ return json.Marshal(res)
}
func (trunks *Trunks) apiTargetAttackResultsDelete(epr *libhttp.EndpointRequest) (resbody []byte, err error) {
@@ -343,6 +391,79 @@ func (trunks *Trunks) getTargetByID(id string) *Target {
return nil
}
+func (trunks *Trunks) getTargetByResultFilename(name string) (t *Target, ht *HttpTarget) {
+ names := strings.Split(name, ".")
+
+ t = trunks.getTargetByID(names[0])
+ if t == nil {
+ return t, nil
+ }
+
+ if len(names) > 0 {
+ ht = t.getHttpTargetByID(names[1])
+ }
+
+ return t, ht
+}
+
+//
+// scanAttackResults scan the environment's ResultsDir for past attack results
+// and add it to each target based on ID.
+// Due to size of output can be big (maybe more than 5000 records), this
+// function only parse the file name and append it to Results field.
+//
+func (trunks *Trunks) scanAttackResults() {
+ logp := "scanAttackResults"
+
+ dir, err := os.Open(trunks.Env.ResultsDir)
+ if err != nil {
+ mlog.Errf("%s: %s\n", logp, err)
+ return
+ }
+
+ fis, err := dir.Readdir(0)
+ if err != nil {
+ mlog.Errf("%s: %s\n", logp, err)
+ return
+ }
+
+ mlog.Outf("--- %s: loading %d files from past results ...\n", logp, len(fis))
+
+ for x, fi := range fis {
+ name := fi.Name()
+ if name == "lost+found" {
+ continue
+ }
+ if name[0] == '.' {
+ continue
+ }
+
+ t, ht := trunks.getTargetByResultFilename(name)
+ if t == nil {
+ mlog.Outf("--- %s %d/%d: Target ID not found for %q\n", logp, x+1, len(fis), name)
+ continue
+ }
+ if ht == nil {
+ mlog.Outf("--- %s %d/%d: HttpTarget ID not found for %q\n", logp, x+1, len(fis), name)
+ continue
+ }
+
+ mlog.Outf("--- %s %d/%d: loading %q with size %d Kb\n", logp, x+1, len(fis), name, fi.Size()/1024)
+
+ ht.addResult(trunks.Env.ResultsDir, name)
+ }
+
+ mlog.Outf("--- %s: all pass results has been loaded ...\n", logp)
+
+ for _, target := range trunks.targets {
+ for _, httpTarget := range target.HttpTargets {
+ sort.Slice(httpTarget.Results, func(x, y int) bool {
+ return httpTarget.Results[x].Name > httpTarget.Results[y].Name
+ })
+ }
+ }
+}
+
func (trunks *Trunks) workerAttackQueue() (err error) {
logp := "workerAttackQueue"