aboutsummaryrefslogtreecommitdiff
path: root/trunks.go
diff options
context:
space:
mode:
authorShulhan <ms@kilabit.info>2021-03-12 02:11:11 +0700
committerShulhan <ms@kilabit.info>2021-03-15 01:52:45 +0700
commitd30bfb7fa1c32984eaba8ad1cf744328812e02b9 (patch)
tree316e089ac24694a3793ff92475be531ccd444b74 /trunks.go
downloadgorankusu-d30bfb7fa1c32984eaba8ad1cf744328812e02b9.tar.xz
trunks: a module for testing HTTP services
Trunks is a library and HTTP service that provide web user interface to test HTTP service, similar to Postman, and for load testing. For the load testing we use vegeta [1] as the backend. [1] https://github.com/tsenart/vegeta
Diffstat (limited to 'trunks.go')
-rw-r--r--trunks.go291
1 files changed, 291 insertions, 0 deletions
diff --git a/trunks.go b/trunks.go
new file mode 100644
index 0000000..9b47fb6
--- /dev/null
+++ b/trunks.go
@@ -0,0 +1,291 @@
+// Copyright 2021, Shulhan <ms@kilabit.info>. All rights reserved.
+// Use of this source code is governed by a BSD-style
+// license that can be found in the LICENSE file.
+
+//
+// Module trunks is a library and HTTP service that provide web user interface
+// to test HTTP service, similar to Postman, and for load testing.
+//
+// For the load testing we use vegeta [1] as the backend.
+//
+// [1] https://github.com/tsenart/vegeta
+//
+package trunks
+
+import (
+ "encoding/json"
+ "fmt"
+ "net/http"
+ "time"
+
+ "github.com/shuLhan/share/lib/debug"
+ libhttp "github.com/shuLhan/share/lib/http"
+ "github.com/shuLhan/share/lib/memfs"
+ "github.com/shuLhan/share/lib/mlog"
+)
+
+const (
+ DefaultAttackDuration = 10 * time.Second
+ DefaultAttackRatePerSecond = 500
+ DefaultAttackTimeout = 30 * time.Second
+ DefaultListenAddress = "127.0.0.1:8217"
+ DefaultMaxAttackDuration = 30 * time.Second
+ DefaultMaxAttackRate = 3000
+)
+
+// 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"
+)
+
+//
+// Trunks is the HTTP server with web user interface for running HTTP test and
+// load testing.
+//
+type Trunks struct {
+ *libhttp.Server
+
+ Env *Environment
+ targets []*Target
+
+ ltrq chan *loadTestingResult
+ finishq chan *loadTestingResult
+ cancelq chan bool
+}
+
+//
+// New create and initialize new Trunks service.
+//
+func New(env *Environment) (trunks *Trunks, err error) {
+ err = env.init()
+ if err != nil {
+ return nil, fmt.Errorf("New: %w", err)
+ }
+
+ trunks = &Trunks{
+ Env: env,
+ }
+
+ httpdOpts := &libhttp.ServerOptions{
+ Options: memfs.Options{
+ Root: "_www",
+ Development: debug.Value >= 2,
+ },
+ Memfs: memfsWWW,
+ Address: env.ListenAddress,
+ }
+
+ trunks.Server, err = libhttp.NewServer(httpdOpts)
+ if err != nil {
+ return nil, fmt.Errorf("New: %w", err)
+ }
+
+ err = trunks.registerHttpApis()
+ if err != nil {
+ return nil, fmt.Errorf("New: %w", err)
+ }
+
+ return trunks, nil
+}
+
+func (trunks *Trunks) RegisterTarget(target *Target) (err error) {
+ if target == nil {
+ return
+ }
+
+ err = target.init()
+ if err != nil {
+ return fmt.Errorf("RegisterTarget: %w", err)
+ }
+
+ trunks.targets = append(trunks.targets, target)
+
+ return nil
+}
+
+//
+// Start the Trunks HTTP server that provide user interface for running and
+// load testing registered Targets.
+//
+func (trunks *Trunks) Start() (err error) {
+ mlog.Outf("starting HTTP server at %s\n", trunks.Env.ListenAddress)
+ return trunks.Server.Start()
+}
+
+//
+// Stop the Trunks HTTP server.
+//
+func (trunks *Trunks) Stop() {
+ mlog.Outf("=== Stopping the Trunks service ...\n")
+
+ err := trunks.Server.Stop(0)
+ if err != nil {
+ mlog.Errf("!!! Stop: %s\n", err)
+ }
+
+ if trunks.isLoadTesting() {
+ trunks.cancelq <- true
+ <-trunks.cancelq
+ }
+}
+
+func (trunks *Trunks) isLoadTesting() (b bool) {
+ trunks.Env.mtx.Lock()
+ if trunks.Env.LoadTestingRunning != nil {
+ b = true
+ }
+ trunks.Env.mtx.Unlock()
+ return b
+}
+
+//
+// registerHttpApis register HTTP APIs to communicate with the Trunks server.
+//
+func (trunks *Trunks) registerHttpApis() (err error) {
+ err = trunks.Server.RegisterEndpoint(&libhttp.Endpoint{
+ Method: libhttp.RequestMethodGet,
+ Path: apiEnvironment,
+ RequestType: libhttp.RequestTypeJSON,
+ ResponseType: libhttp.ResponseTypeJSON,
+ Call: trunks.apiEnvironmentGet,
+ })
+ if err != nil {
+ return err
+ }
+
+ err = trunks.Server.RegisterEndpoint(&libhttp.Endpoint{
+ Method: libhttp.RequestMethodPost,
+ Path: apiTargetAttack,
+ RequestType: libhttp.RequestTypeJSON,
+ ResponseType: libhttp.ResponseTypeJSON,
+ Call: trunks.apiTargetAttack,
+ })
+ if err != nil {
+ return err
+ }
+ err = trunks.Server.RegisterEndpoint(&libhttp.Endpoint{
+ Method: libhttp.RequestMethodDelete,
+ Path: apiTargetAttack,
+ RequestType: libhttp.RequestTypeNone,
+ ResponseType: libhttp.ResponseTypeJSON,
+ Call: trunks.apiTargetAttackCancel,
+ })
+
+ err = trunks.Server.RegisterEndpoint(&libhttp.Endpoint{
+ Method: libhttp.RequestMethodGet,
+ Path: apiTargetAttackResults,
+ RequestType: libhttp.RequestTypeQuery,
+ ResponseType: libhttp.ResponseTypeJSON,
+ Call: trunks.apiTargetAttackResultsGet,
+ })
+ if err != nil {
+ return err
+ }
+ err = trunks.Server.RegisterEndpoint(&libhttp.Endpoint{
+ Method: libhttp.RequestMethodDelete,
+ Path: apiTargetAttackResults,
+ RequestType: libhttp.RequestTypeJSON,
+ ResponseType: libhttp.ResponseTypeJSON,
+ Call: trunks.apiTargetAttackResultsDelete,
+ })
+ if err != nil {
+ return err
+ }
+
+ err = trunks.Server.RegisterEndpoint(&libhttp.Endpoint{
+ Method: libhttp.RequestMethodPost,
+ Path: apiTargetRun,
+ RequestType: libhttp.RequestTypeJSON,
+ ResponseType: libhttp.ResponseTypeJSON,
+ Call: trunks.apiTargetRun,
+ })
+ if err != nil {
+ return err
+ }
+
+ err = trunks.Server.RegisterEndpoint(&libhttp.Endpoint{
+ Method: libhttp.RequestMethodGet,
+ Path: apiTargets,
+ RequestType: libhttp.RequestTypeNone,
+ ResponseType: libhttp.ResponseTypeJSON,
+ Call: trunks.apiTargets,
+ })
+ if err != nil {
+ return err
+ }
+
+ return nil
+}
+
+//
+// apiEnvironmentGet get the Trunks environment including its state.
+//
+func (trunks *Trunks) apiEnvironmentGet(epr *libhttp.EndpointRequest) (resbody []byte, err error) {
+ res := libhttp.EndpointResponse{}
+ res.Code = http.StatusOK
+ res.Data = trunks.Env
+ return json.Marshal(&res)
+}
+
+func (trunks *Trunks) apiTargetAttack(epr *libhttp.EndpointRequest) (resbody []byte, err error) {
+ return resbody, nil
+}
+
+func (trunks *Trunks) apiTargetAttackCancel(epr *libhttp.EndpointRequest) (resbody []byte, err error) {
+ return resbody, nil
+}
+
+func (trunks *Trunks) apiTargetAttackResultsGet(epr *libhttp.EndpointRequest) (resbody []byte, err error) {
+ return resbody, nil
+}
+
+func (trunks *Trunks) apiTargetAttackResultsDelete(epr *libhttp.EndpointRequest) (resbody []byte, err error) {
+ return resbody, nil
+}
+
+func (trunks *Trunks) apiTargetRun(epr *libhttp.EndpointRequest) ([]byte, error) {
+ req := &RunRequest{}
+ err := json.Unmarshal(epr.RequestBody, req)
+ if err != nil {
+ return nil, errInternal(err)
+ }
+ if req.Target == nil {
+ return nil, errInvalidTarget("")
+ }
+
+ target := trunks.getTargetByID(req.Target.ID)
+ if target == nil {
+ return nil, errInvalidTarget(req.Target.ID)
+ }
+
+ if req.HttpTarget != nil {
+ httpTarget := target.getHttpTargetByID(req.HttpTarget.ID)
+ if httpTarget == nil {
+ return nil, errInvalidHttpTarget(req.HttpTarget.ID)
+ }
+
+ return httpTarget.Run(target, req)
+ }
+
+ return nil, errInvalidHttpTarget("")
+}
+
+func (trunks *Trunks) apiTargets(epr *libhttp.EndpointRequest) (resbody []byte, err error) {
+ res := libhttp.EndpointResponse{}
+ res.Code = http.StatusOK
+ res.Data = trunks.targets
+ return json.Marshal(&res)
+}
+
+func (trunks *Trunks) getTargetByID(id string) *Target {
+ for _, target := range trunks.targets {
+ if target.ID == id {
+ return target
+ }
+ }
+ return nil
+}