From 7fed1f75a32c161ba0d296dbb7b6dc14e831336e Mon Sep 17 00:00:00 2001 From: Shulhan Date: Sun, 21 Mar 2021 23:24:46 +0700 Subject: all: implement interface and API to cancel running attack On the top of the page, it will display currently running attack with target name and HTTP target name. User can cancel the attack by clicking "Cancel" button on the right. --- _www/index.css | 17 ++++++++++- _www/index.html | 6 +++- _www/index.js | 82 ++++++++++++++++++++++++++++++++++++++++++++++++------ environment.go | 8 +++--- errors.go | 11 ++++---- example/example.go | 4 +-- trunks.go | 49 ++++++++++++++++++++++---------- 7 files changed, 142 insertions(+), 35 deletions(-) diff --git a/_www/index.css b/_www/index.css index f80f449..ce0eb27 100644 --- a/_www/index.css +++ b/_www/index.css @@ -30,6 +30,7 @@ body { padding: 1em; position: fixed; width: 14em; + border-right: 1px solid lightgrey; } .nav .nav-item { cursor: pointer; @@ -48,6 +49,10 @@ body { height: calc(100% - 4em); overflow: auto; } +.main .mainState { + border: 1px solid lightgrey; + padding: 1em; +} .input { margin-bottom: 1em; } @@ -62,12 +67,22 @@ body { width: calc(100% - 18em); } .mono { - background-color: cornsilk; + background-color: lightgrey; font-family: monospace; padding: 1em; overflow: auto; } +.HttpTarget { + margin-top: 2em; +} + +.HttpTarget h3 { + border-left: 10px solid gold; + margin-left: 0; + padding: 0.5em; +} + .results > .result-name { margin-bottom: 1em; } diff --git a/_www/index.html b/_www/index.html index 56754af..f753ed4 100644 --- a/_www/index.html +++ b/_www/index.html @@ -13,7 +13,7 @@

Trunks

@@ -24,6 +24,10 @@
+
+ Attack running: + - +
diff --git a/_www/index.js b/_www/index.js index 044bd57..e8a5e37 100644 --- a/_www/index.js +++ b/_www/index.js @@ -20,8 +20,15 @@ let _requestTypes = { } async function main() { + await environmentGet() + let fres = await fetch("/_trunks/api/targets") let res = await fres.json() + if (res.code != 200) { + notifError(res.message) + return + } + let targets = res.data let w = "" @@ -39,15 +46,26 @@ async function main() { document.getElementById("nav-content").innerHTML = w } -async function environment() { - let el = document.getElementById("main-content") - +async function environmentGet() { let fres = await fetch("/_trunks/api/environment") let res = await fres.json() + if (res.code != 200) { + notifError(res.message) + return + } _env = res.data - el.innerHTML = ` + if (_env.AttackRunning) { + updateStateAttack( + _env.AttackRunning.Target, + _env.AttackRunning.HttpTarget, + ) + } +} + +async function environmentRender() { + document.getElementById("main-content").innerHTML = `

Environment

@@ -250,6 +268,10 @@ async function run(targetID, httpTargetID) { }) let res = await fres.json() + if (res.code != 200) { + notifError(res.message) + return + } let elResponse = document.getElementById(httpTargetID + "_response") elResponse.innerHTML = JSON.stringify(res, null, 2) @@ -280,6 +302,30 @@ async function attack(targetID, httpTargetID) { }) let res = await fres.json() + if (res.code != 200) { + notifError(res.message) + return + } + + updateStateAttack(target, httpTarget) + + notif(res.message) +} + +async function attackCancel() { + let fres = await fetch("/_trunks/api/target/attack", { + method: "DELETE", + }) + + let res = await fres.json() + if (res.code != 200) { + notifError(res.message) + return + } + + updateStateAttack(null, null) + + notif(res.message) } async function attackResultDelete(name) { @@ -325,6 +371,11 @@ async function attackResultGet(button, name) { let url = "/_trunks/api/target/attack/result?name=" + name let fres = await fetch(url) let res = await fres.json() + if (res.code != 200) { + notifError(res.message) + return + } + let result = res.data el.innerHTML = ` @@ -363,22 +414,37 @@ function onChangeHttpParam(targetID, httpTargetID, key, val) { function notif(msg) { let root = document.getElementById("notif") - let item = document.createElement("div"); + let item = document.createElement("div") item.innerHTML = msg root.appendChild(item) - setTimeout(function() { + setTimeout(function () { root.removeChild(item) }, 5000) } function notifError(msg) { let root = document.getElementById("notif-error") - let item = document.createElement("div"); + let item = document.createElement("div") item.innerHTML = msg root.appendChild(item) - setTimeout(function() { + setTimeout(function () { root.removeChild(item) }, 5000) } + +function updateStateAttack(target, httpTarget) { + let el = document.getElementById("stateAttack") + if (httpTarget) { + el.innerHTML = ` + ${target.Name} / ${httpTarget.Name} +   + + ` + } else { + el.innerHTML = "-" + } +} diff --git a/environment.go b/environment.go index ced921a..7531148 100644 --- a/environment.go +++ b/environment.go @@ -46,7 +46,7 @@ type Environment struct { // AttackRunning will be set to non-nil if there is a load // testing currently running. - AttackRunning *AttackResult + AttackRunning *RunRequest mtx sync.Mutex } @@ -71,11 +71,11 @@ func (env *Environment) init() (err error) { return nil } -func (env *Environment) getRunningAttack() (ar *AttackResult) { +func (env *Environment) getRunningAttack() (rr *RunRequest) { env.mtx.Lock() - ar = env.AttackRunning + rr = env.AttackRunning env.mtx.Unlock() - return ar + return rr } func (env *Environment) isAttackRunning() (yorn bool) { diff --git a/errors.go b/errors.go index 29dcfee..f5f04b4 100644 --- a/errors.go +++ b/errors.go @@ -12,14 +12,15 @@ import ( libhttp "github.com/shuLhan/share/lib/http" ) -func errAttackConflict(ar *AttackResult) error { +func errAttackConflict(rr *RunRequest) error { res := &libhttp.EndpointResponse{ E: liberrors.E{ - Code: http.StatusConflict, - Message: "another attack is already running", - Name: "ERR_ATTACK_CONFLICT", + Code: http.StatusConflict, + Message: fmt.Sprintf(`Another attack is already running: "%s/%s`, + rr.Target.Name, rr.HttpTarget.Name), + Name: "ERR_ATTACK_CONFLICT", }, - Data: ar, + Data: rr, } return res } diff --git a/example/example.go b/example/example.go index 190b39a..9a91ebb 100644 --- a/example/example.go +++ b/example/example.go @@ -95,8 +95,8 @@ func (ex *Example) registerTargets() (err error) { Name: "Example", Opts: &trunks.AttackOptions{ BaseUrl: fmt.Sprintf("http://%s", ex.trunks.Env.ListenAddress), - Duration: 5 * time.Second, - RatePerSecond: 10, + Duration: 300 * time.Second, + RatePerSecond: 1, }, Vars: map[string]string{ "A": "1", diff --git a/trunks.go b/trunks.go index 9b700e9..bc95a42 100644 --- a/trunks.go +++ b/trunks.go @@ -284,7 +284,7 @@ func (trunks *Trunks) apiTargetAttack(epr *libhttp.EndpointRequest) (resbody []b trunks.attackq <- req - msg := fmt.Sprintf("attacking %s/%s with %d RPS for %d seconds", + msg := fmt.Sprintf("Attacking %s/%s with %d RPS for %s seconds", req.Target.Opts.BaseUrl, req.HttpTarget.Path, req.Target.Opts.RatePerSecond, req.Target.Opts.Duration) @@ -292,13 +292,32 @@ func (trunks *Trunks) apiTargetAttack(epr *libhttp.EndpointRequest) (resbody []b res := libhttp.EndpointResponse{} res.Code = http.StatusOK + res.Name = "OK_ATTACK" res.Message = msg return json.Marshal(res) } func (trunks *Trunks) apiTargetAttackCancel(epr *libhttp.EndpointRequest) (resbody []byte, err error) { - return resbody, nil + res := &libhttp.EndpointResponse{} + + rr := trunks.Env.getRunningAttack() + if rr == nil { + res.Code = http.StatusNotFound + res.Message = "No attack is currently running." + res.Name = "ERR_ATTACK_CANCEL_NOT_FOUND" + return nil, res + } + + trunks.cancelq <- true + + res.Code = http.StatusOK + res.Name = "OK_ATTACK_CANCEL" + res.Message = fmt.Sprintf(`Attack on target "%s / %s" has been canceled`, + rr.Target.Name, rr.HttpTarget.Name) + res.Data = rr + + return json.Marshal(res) } func (trunks *Trunks) apiTargetAttackResultGet(epr *libhttp.EndpointRequest) (resbody []byte, err error) { @@ -495,6 +514,8 @@ func (trunks *Trunks) workerAttackQueue() (err error) { logp := "workerAttackQueue" for rr := range trunks.attackq { + trunks.Env.AttackRunning = rr + rr.HttpTarget.PreAttack(rr) isCancelled := false @@ -534,22 +555,22 @@ func (trunks *Trunks) workerAttackQueue() (err error) { // Inform the caller that the attack has been canceled. trunks.cancelq <- true } + } else { + err := rr.result.finish() + if err != nil { + mlog.Errf("%s %s: %s\n", logp, rr.result.Name, err) + } - return nil - } + rr.HttpTarget.Results = append(rr.HttpTarget.Results, rr.result) - err := rr.result.finish() - if err != nil { - mlog.Errf("%s %s: %s\n", logp, rr.result.Name, err) - } - - rr.HttpTarget.Results = append(rr.HttpTarget.Results, rr.result) + sort.Slice(rr.HttpTarget.Results, func(x, y int) bool { + return rr.HttpTarget.Results[x].Name > rr.HttpTarget.Results[y].Name + }) - sort.Slice(rr.HttpTarget.Results, func(x, y int) bool { - return rr.HttpTarget.Results[x].Name > rr.HttpTarget.Results[y].Name - }) + mlog.Outf("%s: %s finished.\n", logp, rr.result.Name) + } - mlog.Outf("%s: %s finished.\n", logp, rr.result.Name) + trunks.Env.AttackRunning = nil } return nil } -- cgit v1.3