diff options
| author | Shulhan <ms@kilabit.info> | 2021-03-21 04:30:07 +0700 |
|---|---|---|
| committer | Shulhan <ms@kilabit.info> | 2021-03-21 04:30:07 +0700 |
| commit | 05474abb07f73aace79bcf339be8e5619dd4564b (patch) | |
| tree | 6cd71065014161f3f4f95aacb6b0508ce20a216c | |
| parent | 77e3074ccbbb01945e06d5608d34d5f39352500e (diff) | |
| download | gorankusu-05474abb07f73aace79bcf339be8e5619dd4564b.tar.xz | |
all: implement API and interface for attack functionality
When the user click "Attack" it will call the API to run the load testing.
If there is load testing currently, it will return with an error.
On success, the result of load testing will be stored on directory
defined in Environment.ResultsDir with file named
<HttpTarget.ID>.<date_time>.<RPS>x<Duration>s.<ResultsSuffix>.bin
This file contains the vegeta.Results.
| -rw-r--r-- | .gitignore | 1 | ||||
| -rw-r--r-- | _www/index.css | 3 | ||||
| -rw-r--r-- | _www/index.js | 78 | ||||
| -rw-r--r-- | environment.go | 20 | ||||
| -rw-r--r-- | errors.go | 23 | ||||
| -rw-r--r-- | example/example.go | 42 | ||||
| -rw-r--r-- | http_target.go | 38 | ||||
| -rw-r--r-- | key_value.go | 10 | ||||
| -rw-r--r-- | load_testing_result.go | 84 | ||||
| -rw-r--r-- | run_request.go | 45 | ||||
| -rw-r--r-- | target.go | 11 | ||||
| -rw-r--r-- | trunks.go | 135 |
12 files changed, 410 insertions, 80 deletions
@@ -1 +1,2 @@ /trunks +/testdata/example diff --git a/_www/index.css b/_www/index.css index 0bc5268..9b1336b 100644 --- a/_www/index.css +++ b/_www/index.css @@ -23,6 +23,8 @@ body { padding: 1em; position: fixed; width: calc(100% - 18em); + height: 100%; + overflow: auto; } .input { margin-bottom: 1em; @@ -42,4 +44,5 @@ body { border-radius: 1em; font-family: monospace; padding: 1em; + overflow: auto; } diff --git a/_www/index.js b/_www/index.js index 9691187..c277139 100644 --- a/_www/index.js +++ b/_www/index.js @@ -88,24 +88,24 @@ function renderTarget(targetID) { <h3> Attack options </h3> <div class="input"> <label> Base URL </label>: - <input id="BaseUrl" readonly="" value="${target.AttackOpts.BaseUrl}"/> + <input id="BaseUrl" readonly="" value="${target.Opts.BaseUrl}"/> </div> <div class="input"> <label> Duration </label>: - <input id="Duration" value="${target.AttackOpts.Duration / 1e9}"/> + <input id="Duration" value="${target.Opts.Duration / 1e9}"/> </div> <div class="input"> <label> Rate per second </label>: - <input id="RatePerSecond" value="${target.AttackOpts.RatePerSecond}"/> + <input id="RatePerSecond" value="${target.Opts.RatePerSecond}"/> </div> <div class="input"> <label> Timeout (seconds) </label>: - <input id="Timeout" value="${target.AttackOpts.Timeout / 1e9}"/> + <input id="Timeout" value="${target.Opts.Timeout / 1e9}"/> </div> </div> ` - if (target.Vars.length > 0) { + if (target.Vars && target.Vars.length > 0) { w += ` <div class='Vars'> <h3>Variables</h3> @@ -122,6 +122,7 @@ function renderTarget(targetID) { } w += "<div class='HttpTargets'>" + for (let x = 0; x < target.HttpTargets.length; x++) { let http = target.HttpTargets[x] @@ -132,6 +133,9 @@ function renderTarget(targetID) { <button onclick="run('${target.ID}', '${http.ID}')"> Run </button> + <button onclick="attack('${target.ID}', '${http.ID}')"> + Attack + </button> </h3> <div class="mono"> ${_requestMethods[http.Method]} ${http.Path} <br/> @@ -150,11 +154,18 @@ function renderTarget(targetID) { } w += ` - <h4>Response</h4> - <pre id="${http.ID}_response" class="mono"> - </pre> - </div> + <h4>Run response</h4> + <pre id="${http.ID}_response" class="mono"> + </pre> ` + + if (http.Results && Object.keys(http.Results).length > 0) { + w += "<h4>Attack results</h4>" + w += renderHttpAttackResults(target, http) + } + + w += "</div>" + } w += "</div>" @@ -195,6 +206,27 @@ function renderHttpTargetParams(target, http) { return w } +function renderHttpAttackResults(target, http) { + let w = `<div id="${http.ID}_results">` + 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> + ` + } + w += "</div>" + return w +} + + async function run(targetID, httpTargetID) { let req = {} req.Target = _targets[targetID] @@ -214,6 +246,33 @@ async function run(targetID, httpTargetID) { elResponse.innerHTML = JSON.stringify(res, null, 2) } +async function attack(targetID, httpTargetID) { + let target = _targets[targetID] + let httpTarget = getHttpTargetByID(target, httpTargetID) + + let req = { + Target: { + ID: target.ID, + Opts: target.Opts, + }, + HttpTarget: { + ID: httpTarget.ID, + Headers: httpTarget.Headers, + Params: httpTarget.Params, + }, + } + + let fres = await fetch("/_trunks/api/target/attack", { + method: "POST", + headers: { + "Content-Type": "application/json", + }, + body: JSON.stringify(req), + }) + + let res = await fres.json() +} + function getHttpTargetByID(target, id) { for (let x = 0; x < target.HttpTargets.length; x++) { if (id == target.HttpTargets[x].ID) { @@ -228,6 +287,7 @@ function onChangeHttpHeader(targetID, httpTargetID, key, val) { let httpTarget = getHttpTargetByID(target, httpTargetID) httpTarget.Headers[key] = val } + function onChangeHttpParam(targetID, httpTargetID, key, val) { let target = _targets[targetID] let httpTarget = getHttpTargetByID(target, httpTargetID) diff --git a/environment.go b/environment.go index cd17e56..393741f 100644 --- a/environment.go +++ b/environment.go @@ -44,10 +44,10 @@ type Environment struct { // uniquely identify results on each run. ResultsSuffix string `ini:"trunks:result:suffix"` - // LoadTestingRunning will be set to non-nil if there is a load + // AttackRunning will be set to non-nil if there is a load // testing currently running. - LoadTestingRunning *loadTestingResult - mtx sync.Mutex + AttackRunning *loadTestingResult + mtx sync.Mutex } func (env *Environment) init() (err error) { @@ -70,3 +70,17 @@ func (env *Environment) init() (err error) { return nil } + +func (env *Environment) getRunningAttack() (ltr *loadTestingResult) { + env.mtx.Lock() + ltr = env.AttackRunning + env.mtx.Unlock() + return ltr +} + +func (env *Environment) isAttackRunning() (yorn bool) { + env.mtx.Lock() + yorn = (env.AttackRunning != nil) + env.mtx.Unlock() + return yorn +} @@ -12,6 +12,29 @@ import ( libhttp "github.com/shuLhan/share/lib/http" ) +func errAttackConflict(ltr *loadTestingResult) error { + res := &libhttp.EndpointResponse{ + E: liberrors.E{ + Code: http.StatusConflict, + Message: "another attack is already running", + Name: "ERR_ATTACK_CONFLICT", + }, + Data: ltr, + } + return res +} + +func errAttackNotAllowed() error { + res := &libhttp.EndpointResponse{ + E: liberrors.E{ + Code: http.StatusNotAcceptable, + Message: "attack is not allowed", + Name: "ERR_ATTACK_NOT_ALLOWED", + }, + } + return res +} + func errInternal(err error) error { res := &libhttp.EndpointResponse{ E: liberrors.E{ diff --git a/example/example.go b/example/example.go index 44c3b5b..33c30d1 100644 --- a/example/example.go +++ b/example/example.go @@ -10,8 +10,10 @@ import ( "net/http" "time" - "git.sr.ht/~shulhan/trunks" libhttp "github.com/shuLhan/share/lib/http" + vegeta "github.com/tsenart/vegeta/v12/lib" + + "git.sr.ht/~shulhan/trunks" ) const ( @@ -19,7 +21,8 @@ const ( ) type Example struct { - trunks *trunks.Trunks + trunks *trunks.Trunks + targetExampleGet vegeta.Target } // @@ -27,7 +30,7 @@ type Example struct { // func New() (ex *Example, err error) { env := &trunks.Environment{ - ResultsDir: "/tmp", + ResultsDir: "testdata/example/", ResultsSuffix: "_trunks_example", } @@ -76,7 +79,7 @@ func (ex *Example) registerEndpoints() (err error) { func (ex *Example) registerTargets() (err error) { targetHttp := &trunks.Target{ Name: "Example HTTP target", - AttackOpts: &trunks.AttackOptions{ + Opts: &trunks.AttackOptions{ BaseUrl: fmt.Sprintf("http://%s", ex.trunks.Env.ListenAddress), Duration: 5 * time.Second, RatePerSecond: 10, @@ -92,7 +95,10 @@ func (ex *Example) registerTargets() (err error) { Params: trunks.KeyValue{ "Param1": "1", }, - Run: ex.runExampleGet, + Run: ex.runExampleGet, + Attack: ex.attackExampleGet, + PreAttack: ex.preattackExampleGet, + AllowAttack: true, }}, } @@ -112,7 +118,7 @@ func (ex *Example) pathExampleGet(epr *libhttp.EndpointRequest) ([]byte, error) func (ex *Example) runExampleGet(target *trunks.Target, req *trunks.RunRequest) ([]byte, error) { if target.HttpClient == nil { - target.HttpClient = libhttp.NewClient(target.AttackOpts.BaseUrl, nil, true) + target.HttpClient = libhttp.NewClient(target.Opts.BaseUrl, nil, true) } _, resbody, err := target.HttpClient.Get( req.HttpTarget.Path, @@ -123,3 +129,27 @@ func (ex *Example) runExampleGet(target *trunks.Target, req *trunks.RunRequest) } return resbody, nil } + +func (ex *Example) preattackExampleGet(rr *trunks.RunRequest) { + ex.targetExampleGet = vegeta.Target{ + Method: rr.HttpTarget.Method.String(), + URL: fmt.Sprintf("%s%s", rr.Target.Opts.BaseUrl, rr.HttpTarget.Path), + Header: rr.HttpTarget.Headers.ToHttpHeader(), + } + + q := rr.HttpTarget.Params.ToUrlValues().Encode() + if len(q) > 0 { + ex.targetExampleGet.URL += "?" + q + } + + fmt.Printf("preattackExampleGet: %+v\n", ex.targetExampleGet) +} + +func (ex *Example) attackExampleGet(rr *trunks.RunRequest) vegeta.Targeter { + return func(tgt *vegeta.Target) error { + rr.HttpTarget.AttackLocker.Lock() + *tgt = ex.targetExampleGet + rr.HttpTarget.AttackLocker.Unlock() + return nil + } +} diff --git a/http_target.go b/http_target.go index c69b950..8bff477 100644 --- a/http_target.go +++ b/http_target.go @@ -12,16 +12,28 @@ import ( vegeta "github.com/tsenart/vegeta/v12/lib" ) -type HttpRunHandler func(target *Target, runRequest *RunRequest) ([]byte, error) -type HttpPreAttackHandler func(target *Target, ht *HttpTarget) vegeta.Targeter +// +// HttpRunHandler defiine the function type that will be called when client +// send request to run the HTTP target. +// +type HttpRunHandler func(tgt *Target, rr *RunRequest) ([]byte, error) + +// +// HttpAttackHandler define the function type that will be called when client +// send request to attack HTTP target. +// +type HttpAttackHandler func(rr *RunRequest) vegeta.Targeter + +// HttpPreAttackHandler define the function type that will be called before +// the actual Attack being called. +type HttpPreAttackHandler func(rr *RunRequest) type HttpTarget struct { // ID of target, optional. // If its empty, it will generated using value from Path. ID string - // Name of target, optional. - // If its empty default to Path. + // Name of target, optional, default to Path. Name string Method libhttp.RequestMethod @@ -30,20 +42,18 @@ type HttpTarget struct { Headers KeyValue Params KeyValue - Run HttpRunHandler `json:"-"` - PreAttack HttpPreAttackHandler `json:"-"` - - // Status of REST. - Status string + Run HttpRunHandler `json:"-"` + Attack HttpAttackHandler `json:"-"` + PreAttack HttpPreAttackHandler `json:"-"` + AttackLocker sync.Mutex `json:"-"` // Use this inside the Attack to lock resource. + Status string // Results contains list of load testing output. Results []*loadTestingResult - // AllowLoadTesting if its true, the "Run load testing" will be showed - // on user interface. - AllowLoadTesting bool - - mtx sync.Mutex + // AllowAttack if its true the "Attack" button will be showed on user + // interface to allow client to run load testing on this HttpTarget. + AllowAttack bool } func (ht *HttpTarget) init() { diff --git a/key_value.go b/key_value.go index 3814f81..7147118 100644 --- a/key_value.go +++ b/key_value.go @@ -5,8 +5,15 @@ import ( "net/url" ) +// +// KeyValue is the simplified type for getting and setting HTTP headers and +// request parameters (either in query or in the parameter body). +// type KeyValue map[string]string +// +// ToHttpHeader convert the KeyValue to the standard http.Header. +// func (kv KeyValue) ToHttpHeader() http.Header { if kv == nil || len(kv) == 0 { return nil @@ -18,6 +25,9 @@ func (kv KeyValue) ToHttpHeader() http.Header { return headers } +// +// ToUrlValues convert the KeyValue to the standard url.Values. +// func (kv KeyValue) ToUrlValues() url.Values { if kv == nil || len(kv) == 0 { return nil diff --git a/load_testing_result.go b/load_testing_result.go index 4018a95..1c0e0cd 100644 --- a/load_testing_result.go +++ b/load_testing_result.go @@ -16,9 +16,10 @@ import ( "sync" "time" + vegeta "github.com/tsenart/vegeta/v12/lib" + libbytes "github.com/shuLhan/share/lib/bytes" "github.com/shuLhan/share/lib/mlog" - vegeta "github.com/tsenart/vegeta/v12/lib" ) const ( @@ -39,8 +40,8 @@ type loadTestingResult struct { HistReport []byte // HistReport the result reported as histogram text. fullpath string - req *request fout *os.File + encoder vegeta.Encoder metrics *vegeta.Metrics hist *vegeta.Histogram } @@ -48,20 +49,19 @@ type loadTestingResult struct { // // newLoadTestingResult create new load testing result from request. // -func newLoadTestingResult(env *Environment, target *Target, httpTarget *HttpTarget, req *request) ( +func newLoadTestingResult(env *Environment, rr *RunRequest) ( ltr *loadTestingResult, err error, ) { ltr = &loadTestingResult{ - TargetID: httpTarget.ID, - req: req, + TargetID: rr.HttpTarget.ID, metrics: &vegeta.Metrics{}, hist: &vegeta.Histogram{}, } ltr.Name = fmt.Sprintf("%s.%s.%dx%s.%s.bin", ltr.TargetID, time.Now().Format(outputSuffixDate), - target.AttackOpts.RatePerSecond, target.AttackOpts.Duration, - req.Env.ResultsSuffix) + rr.Target.Opts.RatePerSecond, rr.Target.Opts.Duration, + env.ResultsSuffix) err = ltr.hist.Buckets.UnmarshalText([]byte(histogramBuckets)) if err != nil { @@ -75,41 +75,21 @@ func newLoadTestingResult(env *Environment, target *Target, httpTarget *HttpTarg return nil, fmt.Errorf("newLoadTestingResult: %w", err) } + ltr.encoder = vegeta.NewEncoder(ltr.fout) + return ltr, nil } -func (ltr *loadTestingResult) init(path string) (err error) { - ltr.fullpath = filepath.Join(path, ltr.Name) - - result, err := ioutil.ReadFile(ltr.fullpath) - if err != nil { - return err - } - - dec := vegeta.NewDecoder(bytes.NewReader(result)) - - ltr.metrics = &vegeta.Metrics{} - ltr.hist = &vegeta.Histogram{} - - err = ltr.hist.Buckets.UnmarshalText([]byte(histogramBuckets)) +func (ltr *loadTestingResult) add(res *vegeta.Result) (err error) { + err = ltr.encoder.Encode(res) if err != nil { - return err + return fmt.Errorf("loadTestingResult.add: %w", err) } - for { - var res vegeta.Result - err = dec.Decode(&res) - if err != nil { - if errors.Is(err, io.EOF) { - break - } - return err - } - ltr.metrics.Add(&res) - ltr.hist.Add(&res) - } + ltr.metrics.Add(res) + ltr.hist.Add(res) - return ltr.finish() + return nil } func (ltr *loadTestingResult) cancel() { @@ -181,6 +161,40 @@ func (ltr *loadTestingResult) finish() (err error) { return nil } +func (ltr *loadTestingResult) init(path string) (err error) { + ltr.fullpath = filepath.Join(path, ltr.Name) + + result, err := ioutil.ReadFile(ltr.fullpath) + if err != nil { + return err + } + + dec := vegeta.NewDecoder(bytes.NewReader(result)) + + ltr.metrics = &vegeta.Metrics{} + ltr.hist = &vegeta.Histogram{} + + err = ltr.hist.Buckets.UnmarshalText([]byte(histogramBuckets)) + if err != nil { + return err + } + + for { + var res vegeta.Result + err = dec.Decode(&res) + if err != nil { + if errors.Is(err, io.EOF) { + break + } + return err + } + ltr.metrics.Add(&res) + ltr.hist.Add(&res) + } + + return ltr.finish() +} + func (ltr *loadTestingResult) pack() (b []byte, err error) { ltr.Lock() b, err = json.Marshal(ltr) diff --git a/run_request.go b/run_request.go index 2d06e23..1f72b96 100644 --- a/run_request.go +++ b/run_request.go @@ -4,7 +4,52 @@ package trunks +import ( + "fmt" + "sync" + "time" + + vegeta "github.com/tsenart/vegeta/v12/lib" +) + type RunRequest struct { + Locker sync.Mutex Target *Target HttpTarget *HttpTarget + result *loadTestingResult +} + +func (rr *RunRequest) String() string { + return fmt.Sprintf("Target:%v HttpTarget:%v\n", rr.Target, rr.HttpTarget) +} + +// +// merge the request parameter into original target and HTTP target. +// +func (rr *RunRequest) merge(env *Environment, origTarget *Target, origHttpTarget *HttpTarget) { + if rr.Target.Opts.Duration > 0 && + rr.Target.Opts.Duration <= env.MaxAttackDuration { + origTarget.Opts.Duration = rr.Target.Opts.Duration + } + + if rr.Target.Opts.RatePerSecond > 0 && + rr.Target.Opts.RatePerSecond <= env.MaxAttackRate { + origTarget.Opts.RatePerSecond = rr.Target.Opts.RatePerSecond + origTarget.Opts.ratePerSecond = vegeta.Rate{ + Freq: rr.Target.Opts.RatePerSecond, + Per: time.Second, + } + } + + if rr.Target.Opts.Timeout > 0 && + rr.Target.Opts.Timeout <= DefaultAttackTimeout { + origTarget.Opts.Timeout = rr.Target.Opts.Timeout + } + + origTarget.Vars = rr.Target.Vars + rr.Target = origTarget + + origHttpTarget.Headers = rr.HttpTarget.Headers + origHttpTarget.Params = rr.HttpTarget.Params + rr.HttpTarget = origHttpTarget } @@ -11,13 +11,12 @@ import ( ) // -// Target contains group of HTTP and/or WebSocket endpoints that can be tested -// by Trunks. +// Target contains group of HttpTarget that can be tested by Trunks. // type Target struct { ID string Name string - AttackOpts *AttackOptions + Opts *AttackOptions Vars map[string]string HttpTargets []*HttpTarget @@ -32,11 +31,11 @@ func (target *Target) init() (err error) { target.ID = generateID(target.Name) - if target.AttackOpts == nil { - target.AttackOpts = &AttackOptions{} + if target.Opts == nil { + target.Opts = &AttackOptions{} } - err = target.AttackOpts.init() + err = target.Opts.init() if err != nil { return err } @@ -16,8 +16,11 @@ import ( "encoding/json" "fmt" "net/http" + "sort" "time" + vegeta "github.com/tsenart/vegeta/v12/lib" + "github.com/shuLhan/share/lib/debug" libhttp "github.com/shuLhan/share/lib/http" "github.com/shuLhan/share/lib/memfs" @@ -43,8 +46,8 @@ const ( ) // -// Trunks is the HTTP server with web user interface for running HTTP test and -// load testing. +// Trunks is the HTTP server with web user interface and APIs for running and +// load testing the registered HTTP endpoints. // type Trunks struct { *libhttp.Server @@ -52,8 +55,7 @@ type Trunks struct { Env *Environment targets []*Target - ltrq chan *loadTestingResult - finishq chan *loadTestingResult + attackq chan *RunRequest cancelq chan bool } @@ -67,7 +69,9 @@ func New(env *Environment) (trunks *Trunks, err error) { } trunks = &Trunks{ - Env: env, + Env: env, + attackq: make(chan *RunRequest, 1), + cancelq: make(chan bool, 1), } httpdOpts := &libhttp.ServerOptions{ @@ -112,6 +116,9 @@ func (trunks *Trunks) RegisterTarget(target *Target) (err error) { // load testing registered Targets. // func (trunks *Trunks) Start() (err error) { + mlog.Outf("Starting attack worker...\n") + go trunks.workerAttackQueue() + mlog.Outf("starting HTTP server at %s\n", trunks.Env.ListenAddress) return trunks.Server.Start() } @@ -135,7 +142,7 @@ func (trunks *Trunks) Stop() { func (trunks *Trunks) isLoadTesting() (b bool) { trunks.Env.mtx.Lock() - if trunks.Env.LoadTestingRunning != nil { + if trunks.Env.AttackRunning != nil { b = true } trunks.Env.mtx.Unlock() @@ -231,8 +238,60 @@ func (trunks *Trunks) apiEnvironmentGet(epr *libhttp.EndpointRequest) (resbody [ return json.Marshal(&res) } +// +// apiTargetAttack run the load testing on HTTP endpoint with target and +// options defined in request. +// func (trunks *Trunks) apiTargetAttack(epr *libhttp.EndpointRequest) (resbody []byte, err error) { - return resbody, nil + if trunks.Env.isAttackRunning() { + return nil, errAttackConflict(trunks.Env.getRunningAttack()) + } + + logp := "apiTargetAttack" + req := &RunRequest{} + + err = json.Unmarshal(epr.RequestBody, req) + if err != nil { + return nil, errInternal(err) + } + if req.Target == nil { + return nil, errInvalidTarget("") + } + + origTarget := trunks.getTargetByID(req.Target.ID) + if origTarget == nil { + return nil, errInvalidTarget(req.Target.ID) + } + + origHttpTarget := origTarget.getHttpTargetByID(req.HttpTarget.ID) + if origTarget == nil { + return nil, errInvalidHttpTarget(req.HttpTarget.ID) + } + + if !origHttpTarget.AllowAttack { + return nil, errAttackNotAllowed() + } + + req.merge(trunks.Env, origTarget, origHttpTarget) + + req.result, err = newLoadTestingResult(trunks.Env, req) + if err != nil { + return nil, err + } + + trunks.attackq <- req + + msg := fmt.Sprintf("attacking %s/%s with %d RPS for %d seconds", + req.Target.Opts.BaseUrl, req.HttpTarget.Path, + req.Target.Opts.RatePerSecond, req.Target.Opts.Duration) + + mlog.Outf("%s: %s\n", logp, msg) + + res := libhttp.EndpointResponse{} + res.Code = http.StatusOK + res.Message = msg + + return json.Marshal(res) } func (trunks *Trunks) apiTargetAttackCancel(epr *libhttp.EndpointRequest) (resbody []byte, err error) { @@ -289,3 +348,65 @@ func (trunks *Trunks) getTargetByID(id string) *Target { } return nil } + +func (trunks *Trunks) workerAttackQueue() (err error) { + logp := "workerAttackQueue" + + for rr := range trunks.attackq { + rr.HttpTarget.PreAttack(rr) + + isCancelled := false + attacker := vegeta.NewAttacker( + vegeta.Timeout(rr.Target.Opts.Timeout), + ) + + for res := range attacker.Attack( + rr.HttpTarget.Attack(rr), + rr.Target.Opts.ratePerSecond, + rr.Target.Opts.Duration, + rr.HttpTarget.ID, + ) { + err = rr.result.add(res) + if err != nil { + attacker.Stop() + rr.result.cancel() + break + } + + select { + case <-trunks.cancelq: + isCancelled = true + default: + } + if isCancelled { + break + } + } + + if err != nil || isCancelled { + attacker.Stop() + rr.result.cancel() + + if err != nil { + mlog.Errf("%s: %s fail: %w.\n", logp, rr.result.Name, err) + } else { + mlog.Outf("%s: %s canceled.\n", logp, rr.result.Name) + trunks.cancelq <- true + } + } else { + err := rr.result.finish() + if err != nil { + mlog.Errf("%s %s: %s\n", logp, rr.result.TargetID, 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 + }) + + mlog.Outf("%s: %s finished.\n", logp, rr.result.Name) + } + } + return nil +} |
