From 372d10a3e1ea01f8d03e44e6ab8be673d05c0773 Mon Sep 17 00:00:00 2001 From: Shulhan Date: Tue, 13 Feb 2024 02:14:35 +0700 Subject: all: move example to root directory The goal is to remove duplicate code in testing and show the example on how to create Gorankusu service from godoc. Implements: https://todo.sr.ht/~shulhan/gorankusu/5 --- example.go | 775 +++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 775 insertions(+) create mode 100644 example.go (limited to 'example.go') diff --git a/example.go b/example.go new file mode 100644 index 0000000..16fa47b --- /dev/null +++ b/example.go @@ -0,0 +1,775 @@ +// SPDX-FileCopyrightText: 2021 M. Shulhan +// SPDX-License-Identifier: GPL-3.0-or-later + +package gorankusu + +import ( + "context" + "encoding/json" + "fmt" + "mime/multipart" + "net/http" + "net/url" + "sync" + "time" + + liberrors "github.com/shuLhan/share/lib/errors" + libhttp "github.com/shuLhan/share/lib/http" + "github.com/shuLhan/share/lib/mlog" + "github.com/shuLhan/share/lib/websocket" + vegeta "github.com/tsenart/vegeta/v12/lib" +) + +const ( + pathExample = `/example` + pathExampleError = `/example/error` + pathExampleNamePage = `/example/:name/page` + pathExampleRawbodyJSON = `/example/rawbody/json` + pathExampleUpload = `/example/upload` +) + +const ( + websocketAddress = `127.0.0.1:28240` +) + +type requestResponse struct { + Method string + URL string + Headers http.Header + Form url.Values + MultipartForm *multipart.Form + Body string +} + +// Example provide an example how to use the Gorankusu library from setting +// it up to creating targets. +// +// To run the example, execute +// +// $ go run ./internal/cmd/gorankusu +// +// It will run a web user interface at http://127.0.0.1:8217 . +type Example struct { + *Gorankusu + wsServer *websocket.Server + + targetExampleErrorGet vegeta.Target + targetExampleGet vegeta.Target + targetExamplePostForm vegeta.Target +} + +// NewExample create, initialize, and setup an example of Gorankusu. +func NewExample() (ex *Example, err error) { + var logp = `NewExample` + + var env = &Environment{ + ResultsDir: `testdata/example/`, + ResultsSuffix: `example`, + } + + ex = &Example{} + + ex.Gorankusu, err = New(env) + if err != nil { + return nil, fmt.Errorf(`%s: %w`, logp, err) + } + + err = ex.registerEndpoints() + if err != nil { + return nil, fmt.Errorf(`%s: %w`, logp, err) + } + + // Create and register endpoint for WebSocket server. + var wsOpts = &websocket.ServerOptions{ + Address: websocketAddress, + } + + ex.wsServer = websocket.NewServer(wsOpts) + + err = ex.registerWebSocketEndpoints() + if err != nil { + return nil, fmt.Errorf(`%s: %w`, logp, err) + } + + // Register target for testing HTTP endpoints. + err = ex.registerTargetHTTP() + if err != nil { + return nil, fmt.Errorf(`%s: %w`, logp, err) + } + + // Register target for testing WebSocket endpoints. + err = ex.registerTargetWebSocket() + if err != nil { + return nil, fmt.Errorf(`%s: %w`, logp, err) + } + + err = ex.registerNavLinks() + if err != nil { + return nil, fmt.Errorf(`%s: %w`, logp, err) + } + + return ex, nil +} + +// Start the Example servers. +func (ex *Example) Start() (err error) { + go func() { + err = ex.wsServer.Start() + if err != nil { + mlog.Errf(`example.Start: %s`, err) + } + }() + + return ex.Gorankusu.Start() +} + +// Stop the Example servers. +func (ex *Example) Stop() { + ex.wsServer.Stop() + ex.Gorankusu.Stop() +} + +// registerEndpoints register HTTP endpoints for testing. +func (ex *Example) registerEndpoints() (err error) { + err = ex.Gorankusu.Httpd.RegisterEndpoint(&libhttp.Endpoint{ + Method: libhttp.RequestMethodGet, + Path: pathExample, + RequestType: libhttp.RequestTypeQuery, + ResponseType: libhttp.ResponseTypeJSON, + Call: ex.pathExampleGet, + }) + if err != nil { + return err + } + + err = ex.Gorankusu.Httpd.RegisterEndpoint(&libhttp.Endpoint{ + Method: libhttp.RequestMethodGet, + Path: pathExampleError, + RequestType: libhttp.RequestTypeQuery, + ResponseType: libhttp.ResponseTypeJSON, + Call: ex.pathExampleErrorGet, + }) + if err != nil { + return err + } + + err = ex.Gorankusu.Httpd.RegisterEndpoint(&libhttp.Endpoint{ + Method: libhttp.RequestMethodPost, + Path: pathExample, + RequestType: libhttp.RequestTypeForm, + ResponseType: libhttp.ResponseTypeJSON, + Call: ex.pathExamplePost, + }) + + err = ex.Gorankusu.Httpd.RegisterEndpoint(&libhttp.Endpoint{ + Method: libhttp.RequestMethodPost, + Path: pathExampleNamePage, + RequestType: libhttp.RequestTypeJSON, + ResponseType: libhttp.ResponseTypeJSON, + Call: ex.pathExamplePost, + }) + + err = ex.Gorankusu.Httpd.RegisterEndpoint(&libhttp.Endpoint{ + Method: libhttp.RequestMethodPost, + Path: pathExampleRawbodyJSON, + RequestType: libhttp.RequestTypeJSON, + ResponseType: libhttp.ResponseTypeJSON, + Call: ex.pathExampleRawbodyJSON, + }) + + err = ex.Gorankusu.Httpd.RegisterEndpoint(&libhttp.Endpoint{ + Method: libhttp.RequestMethodPost, + Path: pathExampleUpload, + RequestType: libhttp.RequestTypeMultipartForm, + ResponseType: libhttp.ResponseTypeJSON, + Call: ex.pathExampleUpload, + }) + + return err +} + +func (ex *Example) registerWebSocketEndpoints() (err error) { + err = ex.wsServer.RegisterTextHandler(http.MethodGet, pathExample, ex.handleWSExampleGet) + if err != nil { + return err + } + return nil +} + +func (ex *Example) registerTargetHTTP() (err error) { + var targetHTTP = &Target{ + ID: `example_http`, + Name: `Example HTTP`, + Hint: `This section provide an example of HTTP endpoints that can be tested and attacked.`, + BaseURL: fmt.Sprintf(`http://%s`, ex.Gorankusu.Env.ListenAddress), + Opts: &AttackOptions{ + Duration: 300 * time.Second, + RatePerSecond: 1, + }, + Vars: KeyFormInput{ + `A`: FormInput{ + Label: `A`, + Hint: `This is the global variabel for all HTTP targets below.`, + Kind: FormInputKindNumber, + Value: `1`, + }, + }, + HTTPTargets: []*HTTPTarget{{ + ID: `http_get`, + Name: `HTTP Get`, + Hint: fmt.Sprintf(`Test or attack endpoint %q using HTTP GET.`, pathExample), + Method: libhttp.RequestMethodGet, + Path: pathExample, + RequestType: libhttp.RequestTypeQuery, + Headers: KeyFormInput{ + `X-Get`: FormInput{ + Label: `X-Get`, + Hint: `Custom HTTP header to be send.`, + Kind: FormInputKindNumber, + Value: `1.1`, + }, + }, + Params: KeyFormInput{ + `Param1`: FormInput{ + Label: `Param1`, + Hint: `Parameter with number.`, + Kind: FormInputKindNumber, + Value: `1`, + }, + }, + Run: ex.runExampleGet, + AllowAttack: true, + Attack: ex.attackExampleGet, + PreAttack: ex.preattackExampleGet, + RequestDumper: requestDumperWithoutDate, + ResponseDumper: responseDumperWithoutDate, + }, { + ID: `http_error_get`, + Name: `HTTP Error Get`, + Hint: fmt.Sprintf(`Test error on endpoint %q using HTTP GET.`, pathExampleError), + Method: libhttp.RequestMethodGet, + Path: pathExampleError, + RequestType: libhttp.RequestTypeQuery, + Headers: KeyFormInput{ + `X-Get`: FormInput{ + Label: `X-Get`, + Hint: `Custom HTTP header to be send.`, + Kind: FormInputKindNumber, + Value: `1.1`, + }, + }, + Params: KeyFormInput{ + `Param1`: FormInput{ + Label: `Param1`, + Hint: `Parameter with number.`, + Kind: FormInputKindNumber, + Value: `1`, + }, + }, + Run: ex.runExampleGet, + AllowAttack: true, + Attack: ex.attackExampleErrorGet, + PreAttack: ex.preattackExampleErrorGet, + RequestDumper: requestDumperWithoutDate, + ResponseDumper: responseDumperWithoutDate, + }, { + ID: `http_post_form`, + Name: `HTTP Post Form`, + Hint: fmt.Sprintf(`Test or attack endpoint %q using HTTP POST.`, pathExample), + Method: libhttp.RequestMethodPost, + Path: pathExample, + RequestType: libhttp.RequestTypeForm, + Headers: KeyFormInput{ + `X-PostForm`: FormInput{ + Label: `X-PostForm`, + Hint: `Custom HTTP header to be send.`, + Kind: FormInputKindNumber, + Value: `1`, + }, + }, + Params: KeyFormInput{ + `Param1`: FormInput{ + Label: `Param1`, + Hint: `Parameter with number.`, + Kind: FormInputKindNumber, + Value: `1`, + }, + `Param2`: FormInput{ + Label: `Param2`, + Hint: `Parameter with string.`, + Kind: FormInputKindString, + Value: `a string`, + }, + }, + Run: ex.runExamplePostForm, + AllowAttack: true, + PreAttack: ex.preattackExamplePostForm, + Attack: ex.attackExamplePostForm, + RequestDumper: requestDumperWithoutDate, + ResponseDumper: responseDumperWithoutDate, + }, { + ID: `http_free_form`, + Name: `HTTP free form`, + Hint: fmt.Sprintf(`Test endpoint %q using custom HTTP method and/or content type.`, pathExample), + Method: libhttp.RequestMethodGet, + Path: pathExample, + RequestType: libhttp.RequestTypeForm, + Headers: KeyFormInput{ + `X-FreeForm`: FormInput{ + Label: `X-FreeForm`, + Hint: `Custom HTTP header to be send.`, + Kind: FormInputKindString, + Value: `1`, + }, + }, + Params: KeyFormInput{ + `Param1`: FormInput{ + Label: `Param1`, + Hint: `Parameter with number.`, + Kind: FormInputKindNumber, + Value: `123`, + }, + }, + RequestDumper: requestDumperWithoutDate, + ResponseDumper: responseDumperWithoutDate, + IsCustomizable: true, + }, { + ID: `http_post_path_binding`, + Name: `HTTP Post path binding`, + Hint: `Test parameter with parameter in path`, + Method: libhttp.RequestMethodPost, + Path: pathExampleNamePage, + RequestType: libhttp.RequestTypeJSON, + Params: KeyFormInput{ + `name`: FormInput{ + Label: `Name`, + Hint: `This parameter send in path.`, + Value: `testname`, + }, + `id`: FormInput{ + Label: `ID`, + Hint: `This parameter send in body as JSON.`, + Value: `123`, + }, + }, + RequestDumper: requestDumperWithoutDate, + ResponseDumper: responseDumperWithoutDate, + }, { + ID: `http_rawbody_json`, + Name: `HTTP raw body - JSON`, + Hint: `Test POST request with manually input raw JSON.`, + Method: libhttp.RequestMethodPost, + Path: pathExampleRawbodyJSON, + RequestType: libhttp.RequestTypeJSON, + RequestDumper: requestDumperWithoutDate, + ResponseDumper: responseDumperWithoutDate, + WithRawBody: true, + }, { + ID: `http_upload`, + Name: `HTTP upload`, + Hint: `Test uploading file`, + Method: libhttp.RequestMethodPost, + Path: pathExampleUpload, + RequestType: libhttp.RequestTypeMultipartForm, + Params: KeyFormInput{ + `file`: FormInput{ + Label: `File`, + Hint: `File to be uploaded.`, + Kind: FormInputKindFile, + FormDataName: func(key string) string { + if key == FormDataFilename { + return `name` + } + return key + }, + }, + `agree`: FormInput{ + Label: `Agree`, + Hint: `Additional parameter along file.`, + Kind: FormInputKindBoolean, + }, + }, + RequestDumper: requestDumperWithoutDate, + ResponseDumper: responseDumperWithoutDate, + }}, + } + + err = ex.Gorankusu.RegisterTarget(targetHTTP) + if err != nil { + return err + } + return nil +} + +func (ex *Example) registerTargetWebSocket() (err error) { + var targetWebSocket = &Target{ + ID: `example_websocket`, + Name: `Example WebSocket`, + Hint: `This section provide an example of WebSocket endpoints that can be tested.`, + BaseURL: fmt.Sprintf(`ws://%s`, websocketAddress), + Opts: &AttackOptions{}, + Vars: KeyFormInput{ + `WebSocketVar`: FormInput{ + Label: `WebSocketVar`, + Kind: FormInputKindString, + Value: `hello`, + }, + }, + WebSocketTargets: []*WebSocketTarget{{ + ID: `ws_get`, + Name: `Similar to HTTP GET`, + Hint: `Test WebSocket endpoint with parameters.`, + Params: KeyFormInput{ + `Param1`: FormInput{ + Label: `Param1`, + Hint: `Parameter with kind is number.`, + Kind: `number`, + Value: `123`, + }, + }, + Run: ex.runWebSocketGet, + }}, + } + + err = ex.Gorankusu.RegisterTarget(targetWebSocket) + if err != nil { + return err + } + + return nil +} + +func (ex *Example) registerNavLinks() (err error) { + var logp = `registerNavLinks` + + err = ex.Gorankusu.RegisterNavLink(&NavLink{ + Text: `Link in IFrame`, + Href: `https://git.sr.ht/~shulhan/gorankusu`, + OpenInIFrame: true, + }) + if err != nil { + return fmt.Errorf(`%s: %w`, logp, err) + } + + err = ex.Gorankusu.RegisterNavLink(&NavLink{ + Text: `Link in new window`, + Href: `https://git.sr.ht/~shulhan/gorankusu`, + }) + if err != nil { + return fmt.Errorf(`%s: %w`, logp, err) + } + + return nil +} + +func (ex *Example) pathExampleGet(epr *libhttp.EndpointRequest) ([]byte, error) { + var data = &requestResponse{ + Method: epr.HttpRequest.Method, + URL: epr.HttpRequest.URL.String(), + Headers: epr.HttpRequest.Header, + Form: epr.HttpRequest.Form, + MultipartForm: epr.HttpRequest.MultipartForm, + Body: string(epr.RequestBody), + } + + var res = libhttp.EndpointResponse{} + res.Code = http.StatusOK + res.Message = pathExample + res.Data = data + + return json.Marshal(&res) +} + +func (ex *Example) pathExampleErrorGet(_ *libhttp.EndpointRequest) ([]byte, error) { + return nil, liberrors.Internal(fmt.Errorf(`server error`)) +} + +func (ex *Example) pathExamplePost(epr *libhttp.EndpointRequest) (resb []byte, err error) { + var data = &requestResponse{ + Method: epr.HttpRequest.Method, + URL: epr.HttpRequest.URL.String(), + Headers: epr.HttpRequest.Header, + Form: epr.HttpRequest.Form, + MultipartForm: epr.HttpRequest.MultipartForm, + Body: string(epr.RequestBody), + } + + var res = libhttp.EndpointResponse{} + res.Code = http.StatusOK + res.Message = pathExample + res.Data = data + + return json.Marshal(&res) +} + +func (ex *Example) pathExampleRawbodyJSON(epr *libhttp.EndpointRequest) (resbody []byte, err error) { + var ( + logp = `pathExampleRawbodyJSON` + data map[string]any + ) + + err = json.Unmarshal(epr.RequestBody, &data) + if err != nil { + return nil, fmt.Errorf(`%s: %w`, logp, err) + } + + var res = libhttp.EndpointResponse{} + res.Code = http.StatusOK + res.Data = data + + resbody, err = json.Marshal(&res) + return resbody, err +} + +func (ex *Example) pathExampleUpload(epr *libhttp.EndpointRequest) (resb []byte, err error) { + var logp = `pathExampleUpload` + + var res = libhttp.EndpointResponse{} + + res.Code = http.StatusOK + res.Data = epr.HttpRequest.MultipartForm.Value + + resb, err = json.MarshalIndent(res, ``, ` `) + if err != nil { + return nil, fmt.Errorf(`%s: %w`, logp, err) + } + + return resb, nil +} + +func (ex *Example) runExampleGet(req *RunRequest) (res *RunResponse, err error) { + if req.Target.HTTPClient == nil { + var httpcOpts = &libhttp.ClientOptions{ + ServerUrl: req.Target.BaseURL, + AllowInsecure: true, + } + req.Target.HTTPClient = libhttp.NewClient(httpcOpts) + } + + res = &RunResponse{} + + var ( + headers = req.HTTPTarget.Headers.ToHTTPHeader() + params = req.HTTPTarget.Params.ToURLValues() + + httpRequest *http.Request + ) + + httpRequest, err = req.Target.HTTPClient.GenerateHttpRequest( + req.HTTPTarget.Method, + req.HTTPTarget.Path, + req.HTTPTarget.RequestType, + headers, + params, + ) + if err != nil { + return nil, err + } + + err = res.SetHTTPRequest(req.HTTPTarget.RequestDumper, httpRequest) + if err != nil { + return nil, err + } + + var httpResponse *http.Response + + httpResponse, _, err = req.Target.HTTPClient.Do(httpRequest) + if err != nil { + return nil, err + } + + err = res.SetHTTPResponse(req.HTTPTarget.ResponseDumper, httpResponse) + if err != nil { + return nil, err + } + + return res, nil +} + +func (ex *Example) preattackExampleErrorGet(rr *RunRequest) { + ex.targetExampleErrorGet = vegeta.Target{ + Method: rr.HTTPTarget.Method.String(), + URL: fmt.Sprintf(`%s%s`, rr.Target.BaseURL, rr.HTTPTarget.Path), + Header: rr.HTTPTarget.Headers.ToHTTPHeader(), + } + + var q = rr.HTTPTarget.Params.ToURLValues().Encode() + if len(q) > 0 { + ex.targetExampleErrorGet.URL += `?` + q + } + + fmt.Printf("preattackExampleErrorGet: %+v\n", ex.targetExampleErrorGet) +} + +func (ex *Example) preattackExampleGet(rr *RunRequest) { + ex.targetExampleGet = vegeta.Target{ + Method: rr.HTTPTarget.Method.String(), + URL: fmt.Sprintf(`%s%s`, rr.Target.BaseURL, rr.HTTPTarget.Path), + Header: rr.HTTPTarget.Headers.ToHTTPHeader(), + } + + var q = rr.HTTPTarget.Params.ToURLValues().Encode() + if len(q) > 0 { + ex.targetExampleGet.URL += `?` + q + } + + fmt.Printf("preattackExampleGet: %+v\n", ex.targetExampleGet) +} + +func (ex *Example) attackExampleErrorGet(rr *RunRequest) vegeta.Targeter { + return func(tgt *vegeta.Target) error { + rr.HTTPTarget.Lock() + *tgt = ex.targetExampleErrorGet + rr.HTTPTarget.Unlock() + return nil + } +} + +func (ex *Example) attackExampleGet(rr *RunRequest) vegeta.Targeter { + return func(tgt *vegeta.Target) error { + rr.HTTPTarget.Lock() + *tgt = ex.targetExampleGet + rr.HTTPTarget.Unlock() + return nil + } +} + +func (ex *Example) runExamplePostForm(req *RunRequest) (res *RunResponse, err error) { + if req.Target.HTTPClient == nil { + var httpcOpts = &libhttp.ClientOptions{ + ServerUrl: req.Target.BaseURL, + AllowInsecure: true, + } + req.Target.HTTPClient = libhttp.NewClient(httpcOpts) + } + + res = &RunResponse{} + + var ( + headers = req.HTTPTarget.Headers.ToHTTPHeader() + params = req.HTTPTarget.Params.ToURLValues() + + httpRequest *http.Request + ) + + httpRequest, err = req.Target.HTTPClient.GenerateHttpRequest( + req.HTTPTarget.Method, + req.HTTPTarget.Path, + req.HTTPTarget.RequestType, + headers, + params, + ) + if err != nil { + return nil, err + } + + err = res.SetHTTPRequest(req.HTTPTarget.RequestDumper, httpRequest) + if err != nil { + return nil, err + } + + var httpResponse *http.Response + + httpResponse, _, err = req.Target.HTTPClient.Do(httpRequest) + if err != nil { + return nil, err + } + + err = res.SetHTTPResponse(req.HTTPTarget.ResponseDumper, httpResponse) + if err != nil { + return nil, err + } + + return res, nil +} + +func (ex *Example) preattackExamplePostForm(rr *RunRequest) { + ex.targetExamplePostForm = vegeta.Target{ + Method: rr.HTTPTarget.Method.String(), + URL: fmt.Sprintf(`%s%s`, rr.Target.BaseURL, rr.HTTPTarget.Path), + Header: rr.HTTPTarget.Headers.ToHTTPHeader(), + } + + var q = rr.HTTPTarget.Params.ToURLValues().Encode() + if len(q) > 0 { + ex.targetExamplePostForm.Body = []byte(q) + } + + fmt.Printf("preattackExamplePostForm: %+v\n", ex.targetExamplePostForm) +} + +func (ex *Example) attackExamplePostForm(rr *RunRequest) vegeta.Targeter { + return func(tgt *vegeta.Target) error { + rr.HTTPTarget.Lock() + *tgt = ex.targetExamplePostForm + rr.HTTPTarget.Unlock() + return nil + } +} + +func (ex *Example) handleWSExampleGet(_ context.Context, req *websocket.Request) (res websocket.Response) { + res.ID = req.ID + res.Code = http.StatusOK + res.Body = req.Body + return res +} + +func (ex *Example) runWebSocketGet(rr *RunRequest) (res interface{}, err error) { + var wg sync.WaitGroup + + var wsc = &websocket.Client{ + Endpoint: `ws://` + websocketAddress, + HandleText: func(_ *websocket.Client, frame *websocket.Frame) error { + res = frame.Payload() + wg.Done() + return nil + }, + } + + err = wsc.Connect() + if err != nil { + return nil, err + } + + var body []byte + + body, err = json.Marshal(rr.WebSocketTarget.Params) + if err != nil { + return nil, err + } + + var req = websocket.Request{ + ID: uint64(time.Now().UnixNano()), + Method: http.MethodGet, + Target: pathExample, + Body: string(body), + } + + var reqtext []byte + + reqtext, err = json.Marshal(&req) + if err != nil { + return nil, err + } + + err = wsc.SendText(reqtext) + if err != nil { + return nil, err + } + wg.Add(1) + wg.Wait() + + _ = wsc.Close() + + return res, nil +} + +func requestDumperWithoutDate(req *http.Request) ([]byte, error) { + req.Header.Del(libhttp.HeaderDate) + return DumpHTTPRequest(req) +} + +func responseDumperWithoutDate(resp *http.Response) ([]byte, error) { + resp.Header.Del(libhttp.HeaderDate) + return DumpHTTPResponse(resp) +} -- cgit v1.3