From 7a2558b25ad7b52b2005f1c813bd253d898a84b3 Mon Sep 17 00:00:00 2001 From: Aykhan Shahsuvarov Date: Sat, 25 May 2024 20:26:20 +0400 Subject: [PATCH] =?UTF-8?q?=F0=9F=8E=89first=20commit?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .gitignore | 1 + README.md | 0 config.json | 21 ++ config/config.go | 140 +++++++++++++ custom_errors/errors.go | 114 +++++++++++ custom_errors/formaters.go | 76 +++++++ go.mod | 25 +++ go.sum | 45 +++++ main.go | 106 ++++++++++ readers/cli.go | 85 ++++++++ readers/json.go | 33 +++ requests/requests.go | 402 +++++++++++++++++++++++++++++++++++++ utils/print.go | 60 ++++++ utils/slice.go | 18 ++ validation/validator.go | 57 ++++++ 15 files changed, 1183 insertions(+) create mode 100644 .gitignore create mode 100644 README.md create mode 100644 config.json create mode 100644 config/config.go create mode 100644 custom_errors/errors.go create mode 100644 custom_errors/formaters.go create mode 100644 go.mod create mode 100644 go.sum create mode 100644 main.go create mode 100644 readers/cli.go create mode 100644 readers/json.go create mode 100644 requests/requests.go create mode 100644 utils/print.go create mode 100644 utils/slice.go create mode 100644 validation/validator.go diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..5faa436 --- /dev/null +++ b/.gitignore @@ -0,0 +1 @@ +dodo \ No newline at end of file diff --git a/README.md b/README.md new file mode 100644 index 0000000..e69de29 diff --git a/config.json b/config.json new file mode 100644 index 0000000..abc1a26 --- /dev/null +++ b/config.json @@ -0,0 +1,21 @@ +{ + "method": "GET", + "url": "https://example.com", + "timeout": 10000, + "dodos_count": 1, + "request_count": 1000, + "params": {}, + "headers": {}, + "cookies": {}, + "body": "", + "proxies": [ + { + "url": "http://example:8080", + "username": "username", + "password": "password" + }, + { + "url": "http://example.com:8080" + } + ] +} \ No newline at end of file diff --git a/config/config.go b/config/config.go new file mode 100644 index 0000000..b53c5fd --- /dev/null +++ b/config/config.go @@ -0,0 +1,140 @@ +package config + +import ( + "fmt" + "os" + "time" + + // "github.com/aykhans/dodo/utils" + "github.com/jedib0t/go-pretty/v6/table" +) + +const ( + VERSION = "0.0.1" + DefaultUserAgent = "Dodo/" + VERSION + ProxyCheckURL = "https://google.com" + DefaultMethod = "GET" + DefaultTimeout = 10000 // Milliseconds (10 seconds) + DefaultDodosCount = 1 + DefaultRequestCount = 1000 + MaxDodosCountForProxies = 20 // Max dodos count for proxy check +) + +type IConfig interface { + MergeConfigs(newConfig IConfig) IConfig +} + +type ProxySlice []map[string]string + +type DodoConfig struct { + Method string + URL string + Timeout time.Duration + DodosCount int + RequestCount int + Params map[string]string + Headers map[string]string + Cookies map[string]string + Proxies ProxySlice + Body string +} + +func (config *DodoConfig) Print() { + t := table.NewWriter() + t.SetOutputMirror(os.Stdout) + t.SetStyle(table.StyleLight) + t.AppendRow(table.Row{ + "Method", "URL", "Timeout", "Dodos", + "Request Count", "Params Count", + "Headers Count", "Cookies Count", + "Proxies Count", "Body"}) + t.AppendSeparator() + t.AppendRow(table.Row{ + config.Method, config.URL, + fmt.Sprintf("%dms", config.Timeout/time.Millisecond), + config.DodosCount, config.RequestCount, + len(config.Params), len(config.Headers), + len(config.Cookies), len(config.Proxies), config.Body}) + t.Render() +} + +type Config struct { + Method string `json:"method" validate:"http_method"` // custom validations: http_method + URL string `json:"url" validate:"http_url,required"` + Timeout int `json:"timeout" validate:"gte=1,lte=100000"` + DodosCount int `json:"dodos_count" validate:"gte=1"` + RequestCount int `json:"request_count" validation_name:"request-count" validate:"gte=1"` +} + +func (config *Config) MergeConfigs(newConfig *Config) { + if newConfig.Method != "" { + config.Method = newConfig.Method + } + if newConfig.URL != "" { + config.URL = newConfig.URL + } + if newConfig.Timeout != 0 { + config.Timeout = newConfig.Timeout + } + if newConfig.DodosCount != 0 { + config.DodosCount = newConfig.DodosCount + } + if newConfig.RequestCount != 0 { + config.RequestCount = newConfig.RequestCount + } +} + +func (config *Config) SetDefaults() { + if config.Method == "" { + config.Method = DefaultMethod + } + if config.Timeout == 0 { + config.Timeout = DefaultTimeout + } + if config.DodosCount == 0 { + config.DodosCount = DefaultDodosCount + } + if config.RequestCount == 0 { + config.RequestCount = DefaultRequestCount + } +} + +type JSONConfig struct { + Config + Params map[string]string `json:"params"` + Headers map[string]string `json:"headers"` + Cookies map[string]string `json:"cookies"` + Proxies ProxySlice `json:"proxies" validate:"url_map_slice"` + Body string `json:"body"` +} + +func (config *JSONConfig) MergeConfigs(newConfig *JSONConfig) { + config.Config.MergeConfigs(&newConfig.Config) + if len(newConfig.Params) != 0 { + config.Params = newConfig.Params + } + if len(newConfig.Headers) != 0 { + config.Headers = newConfig.Headers + } + if len(newConfig.Cookies) != 0 { + config.Cookies = newConfig.Cookies + } + if newConfig.Body != "" { + config.Body = newConfig.Body + } + if len(newConfig.Proxies) != 0 { + config.Proxies = newConfig.Proxies + } +} + +type CLIConfig struct { + Config + ConfigFile string `validation_name:"config-file" validate:"omitempty,filepath"` +} + +func (config *CLIConfig) MergeConfigs(newConfig *CLIConfig) { + config.Config.MergeConfigs(&newConfig.Config) + if newConfig.ConfigFile != "" { + config.ConfigFile = newConfig.ConfigFile + } +} diff --git a/custom_errors/errors.go b/custom_errors/errors.go new file mode 100644 index 0000000..6bc7ab2 --- /dev/null +++ b/custom_errors/errors.go @@ -0,0 +1,114 @@ +package customerrors + +import ( + "errors" + "fmt" + + "github.com/go-playground/validator/v10" +) + +var ( + ErrInvalidJSON = errors.New("invalid JSON file") + ErrInvalidFile = errors.New("invalid file") +) + +func As(err error, target any) bool { + return errors.As(err, target) +} + +func Is(err, target error) bool { + return errors.Is(err, target) +} + +type Error interface { + Error() string + Unwrap() error +} + +type TypeError struct { + Expected string + Received string + Field string + err error +} + +func NewTypeError(expected, received, field string, err error) *TypeError { + return &TypeError{ + Expected: expected, + Received: received, + Field: field, + err: err, + } +} + +func (e *TypeError) Error() string { + return "Expected " + e.Expected + " but received " + e.Received + " in field " + e.Field +} + +func (e *TypeError) Unwrap() error { + return e.err +} + +type InvalidFileError struct { + FileName string + err error +} + +func NewInvalidFileError(fileName string, err error) *InvalidFileError { + return &InvalidFileError{ + FileName: fileName, + err: err, + } +} + +func (e *InvalidFileError) Error() string { + return "Invalid file: " + e.FileName +} + +func (e *InvalidFileError) Unwrap() error { + return e.err +} + +type FileNotFoundError struct { + FileName string + err error +} + +func NewFileNotFoundError(fileName string, err error) *FileNotFoundError { + return &FileNotFoundError{ + FileName: fileName, + err: err, + } +} + +func (e *FileNotFoundError) Error() string { + return "File not found: " + e.FileName +} + +func (e *FileNotFoundError) Unwrap() error { + return e.err +} + +type ValidationErrors struct { + MapErrors map[string]string + errors validator.ValidationErrors +} + +func NewValidationErrors(errsMap map[string]string, errs validator.ValidationErrors) *ValidationErrors { + return &ValidationErrors{ + MapErrors: errsMap, + errors: errs, + } +} + +func (errs *ValidationErrors) Error() string { + var errorsStr string + for k, v := range errs.MapErrors { + errorsStr += fmt.Sprintf("[%s]: %s\n", k, v) + } + return errorsStr +} + +func (errs *ValidationErrors) Unwrap() error { + return errs.errors +} diff --git a/custom_errors/formaters.go b/custom_errors/formaters.go new file mode 100644 index 0000000..2050656 --- /dev/null +++ b/custom_errors/formaters.go @@ -0,0 +1,76 @@ +package customerrors + +import ( + "fmt" + "net" + "net/url" + "strings" + + // "github.com/aykhans/dodo/config" + "github.com/aykhans/dodo/config" + "github.com/go-playground/validator/v10" +) + +func OSErrorFormater(err error) error { + errStr := err.Error() + if strings.Contains(errStr, "no such file or directory") { + fileName1 := strings.Index(errStr, "open") + fileName2 := strings.LastIndex(errStr, ":") + return NewFileNotFoundError(errStr[fileName1+5:fileName2], err) + } + return ErrInvalidFile +} + +func CobraErrorFormater(err error) error { + return err +} + +func ValidationErrorsFormater(errs validator.ValidationErrors) error { + errsStr := make(map[string]string) + for _, err := range errs { + switch err.Tag() { + case "required": + errsStr[err.Field()] = fmt.Sprintf("Field \"%s\" is required", err.Field()) + case "gte": + errsStr[err.Field()] = fmt.Sprintf("Value of \"%s\" must be greater than or equal to \"%s\"", err.Field(), err.Param()) + case "lte": + errsStr[err.Field()] = fmt.Sprintf("Value of \"%s\" must be less than or equal to \"%s\"", err.Field(), err.Param()) + case "filepath": + errsStr[err.Field()] = fmt.Sprintf("Invalid file path for \"%s\" field: \"%s\"", err.Field(), err.Value()) + case "http_url": + errsStr[err.Field()] = fmt.Sprintf("Invalid URL for \"%s\" field: \"%s\"", err.Field(), err.Value()) + // --------------------------------------| Custom validations |-------------------------------------- + case "http_method": + errsStr[err.Field()] = fmt.Sprintf("Invalid HTTP method for \"%s\" field: \"%s\"", err.Field(), err.Value()) + case "url_map_slice": + values := err.Value().(config.ProxySlice) + for i, value := range values { + if _, ok := value["url"]; !ok { + errsStr[fmt.Sprintf("%s[%d]", err.Field(), i)] = fmt.Sprintf("Field \"url\" is required for \"%s\" field", err.Field()) + } else { + errsStr[fmt.Sprintf("%s[%d]", err.Field(), i)] = fmt.Sprintf("Invalid url for \"%s\" field: \"%s\"", err.Field(), value["url"]) + } + } + case "string_bool": + errsStr[err.Field()] = fmt.Sprintf("Invalid value for \"%s\" field: \"%s\"", err.Field(), err.Value()) + default: + errsStr[err.Field()] = fmt.Sprintf("Invalid value for \"%s\" field: \"%s\"", err.Field(), err.Value()) + } + } + return NewValidationErrors(errsStr, errs) +} + +func RequestErrorsFormater(err error) string { + switch e := err.(type) { + case *url.Error: + if netErr, ok := e.Err.(net.Error); ok && netErr.Timeout() { + return "Timeout Error" + } + if strings.Contains(e.Error(), "http: ContentLength=") { + println(e.Error()) + return "Empty Body Error" + } + // TODO: Add more cases + } + return "Unknown Error" +} diff --git a/go.mod b/go.mod new file mode 100644 index 0000000..7f207d0 --- /dev/null +++ b/go.mod @@ -0,0 +1,25 @@ +module github.com/aykhans/dodo + +go 1.22.3 + +require ( + github.com/go-playground/validator/v10 v10.20.0 + github.com/jedib0t/go-pretty/v6 v6.5.9 + github.com/spf13/cobra v1.8.0 + github.com/spf13/pflag v1.0.5 + golang.org/x/net v0.21.0 +) + +require ( + github.com/gabriel-vasile/mimetype v1.4.3 // indirect + github.com/go-playground/locales v0.14.1 // indirect + github.com/go-playground/universal-translator v0.18.1 // indirect + github.com/inconshreveable/mousetrap v1.1.0 // indirect + github.com/leodido/go-urn v1.4.0 // indirect + github.com/mattn/go-runewidth v0.0.15 // indirect + github.com/rivo/uniseg v0.2.0 // indirect + golang.org/x/crypto v0.19.0 // indirect + golang.org/x/sys v0.17.0 // indirect + golang.org/x/term v0.17.0 // indirect + golang.org/x/text v0.14.0 // indirect +) diff --git a/go.sum b/go.sum new file mode 100644 index 0000000..4fdb4ae --- /dev/null +++ b/go.sum @@ -0,0 +1,45 @@ +github.com/cpuguy83/go-md2man/v2 v2.0.3/go.mod h1:tgQtvFlXSQOSOSIRvRPT7W67SCa46tRHOmNcaadrF8o= +github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c= +github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= +github.com/gabriel-vasile/mimetype v1.4.3 h1:in2uUcidCuFcDKtdcBxlR0rJ1+fsokWf+uqxgUFjbI0= +github.com/gabriel-vasile/mimetype v1.4.3/go.mod h1:d8uq/6HKRL6CGdk+aubisF/M5GcPfT7nKyLpA0lbSSk= +github.com/go-playground/assert/v2 v2.2.0 h1:JvknZsQTYeFEAhQwI4qEt9cyV5ONwRHC+lYKSsYSR8s= +github.com/go-playground/assert/v2 v2.2.0/go.mod h1:VDjEfimB/XKnb+ZQfWdccd7VUvScMdVu0Titje2rxJ4= +github.com/go-playground/locales v0.14.1 h1:EWaQ/wswjilfKLTECiXz7Rh+3BjFhfDFKv/oXslEjJA= +github.com/go-playground/locales v0.14.1/go.mod h1:hxrqLVvrK65+Rwrd5Fc6F2O76J/NuW9t0sjnWqG1slY= +github.com/go-playground/universal-translator v0.18.1 h1:Bcnm0ZwsGyWbCzImXv+pAJnYK9S473LQFuzCbDbfSFY= +github.com/go-playground/universal-translator v0.18.1/go.mod h1:xekY+UJKNuX9WP91TpwSH2VMlDf28Uj24BCp08ZFTUY= +github.com/go-playground/validator/v10 v10.20.0 h1:K9ISHbSaI0lyB2eWMPJo+kOS/FBExVwjEviJTixqxL8= +github.com/go-playground/validator/v10 v10.20.0/go.mod h1:dbuPbCMFw/DrkbEynArYaCwl3amGuJotoKCe95atGMM= +github.com/inconshreveable/mousetrap v1.1.0 h1:wN+x4NVGpMsO7ErUn/mUI3vEoE6Jt13X2s0bqwp9tc8= +github.com/inconshreveable/mousetrap v1.1.0/go.mod h1:vpF70FUmC8bwa3OWnCshd2FqLfsEA9PFc4w1p2J65bw= +github.com/jedib0t/go-pretty/v6 v6.5.9 h1:ACteMBRrrmm1gMsXe9PSTOClQ63IXDUt03H5U+UV8OU= +github.com/jedib0t/go-pretty/v6 v6.5.9/go.mod h1:zbn98qrYlh95FIhwwsbIip0LYpwSG8SUOScs+v9/t0E= +github.com/leodido/go-urn v1.4.0 h1:WT9HwE9SGECu3lg4d/dIA+jxlljEa1/ffXKmRjqdmIQ= +github.com/leodido/go-urn v1.4.0/go.mod h1:bvxc+MVxLKB4z00jd1z+Dvzr47oO32F/QSNjSBOlFxI= +github.com/mattn/go-runewidth v0.0.15 h1:UNAjwbU9l54TA3KzvqLGxwWjHmMgBUVhBiTjelZgg3U= +github.com/mattn/go-runewidth v0.0.15/go.mod h1:Jdepj2loyihRzMpdS35Xk/zdY8IAYHsh153qUoGf23w= +github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM= +github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= +github.com/rivo/uniseg v0.2.0 h1:S1pD9weZBuJdFmowNwbpi7BJ8TNftyUImj/0WQi72jY= +github.com/rivo/uniseg v0.2.0/go.mod h1:J6wj4VEh+S6ZtnVlnTBMWIodfgj8LQOQFoIToxlJtxc= +github.com/russross/blackfriday/v2 v2.1.0/go.mod h1:+Rmxgy9KzJVeS9/2gXHxylqXiyQDYRxCVz55jmeOWTM= +github.com/spf13/cobra v1.8.0 h1:7aJaZx1B85qltLMc546zn58BxxfZdR/W22ej9CFoEf0= +github.com/spf13/cobra v1.8.0/go.mod h1:WXLWApfZ71AjXPya3WOlMsY9yMs7YeiHhFVlvLyhcho= +github.com/spf13/pflag v1.0.5 h1:iy+VFUOCP1a+8yFto/drg2CJ5u0yRoB7fZw3DKv/JXA= +github.com/spf13/pflag v1.0.5/go.mod h1:McXfInJRrz4CZXVZOBLb0bTZqETkiAhM9Iw0y3An2Bg= +github.com/stretchr/testify v1.8.4 h1:CcVxjf3Q8PM0mHUKJCdn+eZZtm5yQwehR5yeSVQQcUk= +github.com/stretchr/testify v1.8.4/go.mod h1:sz/lmYIOXD/1dqDmKjjqLyZ2RngseejIcXlSw2iwfAo= +golang.org/x/crypto v0.19.0 h1:ENy+Az/9Y1vSrlrvBSyna3PITt4tiZLf7sgCjZBX7Wo= +golang.org/x/crypto v0.19.0/go.mod h1:Iy9bg/ha4yyC70EfRS8jz+B6ybOBKMaSxLj6P6oBDfU= +golang.org/x/net v0.21.0 h1:AQyQV4dYCvJ7vGmJyKki9+PBdyvhkSd8EIx/qb0AYv4= +golang.org/x/net v0.21.0/go.mod h1:bIjVDfnllIU7BJ2DNgfnXvpSvtn8VRwhlsaeUTyUS44= +golang.org/x/sys v0.17.0 h1:25cE3gD+tdBA7lp7QfhuV+rJiE9YXTcS3VG1SqssI/Y= +golang.org/x/sys v0.17.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA= +golang.org/x/term v0.17.0 h1:mkTF7LCd6WGJNL3K1Ad7kwxNfYAW6a8a8QqtMblp/4U= +golang.org/x/term v0.17.0/go.mod h1:lLRBjIVuehSbZlaOtGMbcMncT+aqLLLmKrsjNrUguwk= +golang.org/x/text v0.14.0 h1:ScX5w1eTa3QqT8oi6+ziP7dTV1S2+ALU0bI+0zXKWiQ= +golang.org/x/text v0.14.0/go.mod h1:18ZOQIKpY8NJVqYksKHtTdi31H5itFRjB5/qKTNYzSU= +gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= +gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA= +gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= diff --git a/main.go b/main.go new file mode 100644 index 0000000..5bc9ff5 --- /dev/null +++ b/main.go @@ -0,0 +1,106 @@ +package main + +import ( + "os" + "time" + + "github.com/aykhans/dodo/config" + "github.com/aykhans/dodo/custom_errors" + "github.com/aykhans/dodo/readers" + "github.com/aykhans/dodo/requests" + "github.com/aykhans/dodo/utils" + "github.com/aykhans/dodo/validation" + goValidator "github.com/go-playground/validator/v10" + "github.com/jedib0t/go-pretty/v6/table" +) + +func main() { + validator := validation.NewValidator() + conf := config.Config{} + jsonConf := config.JSONConfig{} + + cliConf, err := readers.CLIConfigReader() + if err != nil || cliConf == nil { + os.Exit(0) + } + if err := validator.StructPartial(cliConf, "ConfigFile"); err != nil { + utils.PrintErrAndExit( + customerrors.ValidationErrorsFormater( + err.(goValidator.ValidationErrors), + ), + ) + } + if cliConf.ConfigFile != "" { + jsonConfNew, err := readers.JSONConfigReader(cliConf.ConfigFile) + if err != nil { + utils.PrintErrAndExit(err) + } + if err := validator.StructPartial(jsonConfNew, "Proxies"); err != nil { + utils.PrintErrAndExit( + customerrors.ValidationErrorsFormater( + err.(goValidator.ValidationErrors), + ), + ) + } + jsonConf = *jsonConfNew + conf.MergeConfigs(&jsonConf.Config) + } + + conf.MergeConfigs(&cliConf.Config) + conf.SetDefaults() + if err := validator.Struct(conf); err != nil { + utils.PrintErrAndExit( + customerrors.ValidationErrorsFormater( + err.(goValidator.ValidationErrors), + ), + ) + } + + dodoConf := &config.DodoConfig{ + Method: conf.Method, + URL: conf.URL, + Timeout: time.Duration(conf.Timeout) * time.Millisecond, + DodosCount: conf.DodosCount, + RequestCount: conf.RequestCount, + Params: jsonConf.Params, + Headers: jsonConf.Headers, + Cookies: jsonConf.Cookies, + Proxies: jsonConf.Proxies, + Body: jsonConf.Body, + } + + dodoConf.Print() + responses, err := requests.Run(dodoConf) + if err != nil { + utils.PrintErrAndExit(err) + } + + t := table.NewWriter() + t.SetOutputMirror(os.Stdout) + t.SetStyle(table.StyleLight) + t.AppendHeader(table.Row{ + "Response", + "Count", + "Min Time", + "Max Time", + "Average Time", + }) + for _, mergedResponse := range responses.MergeDodoResponses() { + t.AppendRow(table.Row{ + mergedResponse.Response, + mergedResponse.Count, + mergedResponse.MinTime, + mergedResponse.MaxTime, + mergedResponse.AvgTime, + }) + t.AppendSeparator() + } + t.AppendFooter(table.Row{ + "Total", + responses.Len(), + responses.MinTime(), + responses.MaxTime(), + responses.AvgTime(), + }) + t.Render() +} diff --git a/readers/cli.go b/readers/cli.go new file mode 100644 index 0000000..d459792 --- /dev/null +++ b/readers/cli.go @@ -0,0 +1,85 @@ +package readers + +import ( + "fmt" + + "github.com/aykhans/dodo/config" + customerrors "github.com/aykhans/dodo/custom_errors" + "github.com/aykhans/dodo/utils" + "github.com/spf13/cobra" + "github.com/spf13/pflag" +) + +func CLIConfigReader() (*config.CLIConfig, error) { + var ( + returnNil = false + cliConfig = &config.CLIConfig{} + dodosCount int + requestCount int + timeout int + rootCmd = &cobra.Command{ + Use: "dodo [flags]", + Example: ` Simple usage only with URL: + dodo -u https://example.com + + Simple usage with config file: + dodo -c /path/to/config/file/config.json + + Usage with all flags: + dodo -c /path/to/config/file/config.json -u https://example.com -m POST -d 10 -r 1000 -t 2000`, + Short: ` +██████████ ███████ ██████████ ███████ +░░███░░░░███ ███░░░░░███ ░░███░░░░███ ███░░░░░███ + ░███ ░░███ ███ ░░███ ░███ ░░███ ███ ░░███ + ░███ ░███░███ ░███ ░███ ░███░███ ░███ + ░███ ░███░███ ░███ ░███ ░███░███ ░███ + ░███ ███ ░░███ ███ ░███ ███ ░░███ ███ + ██████████ ░░░███████░ ██████████ ░░░███████░ +░░░░░░░░░░ ░░░░░░░ ░░░░░░░░░░ ░░░░░░░ +`, + Run: func(cmd *cobra.Command, args []string) {}, + SilenceErrors: true, + SilenceUsage: true, + Version: config.VERSION, + } + ) + + rootCmd.Flags().StringVarP(&cliConfig.ConfigFile, "config-file", "c", "", "Path to the config file") + rootCmd.Flags().StringVarP(&cliConfig.Method, "method", "m", "", fmt.Sprintf("HTTP Method (default %s)", config.DefaultMethod)) + rootCmd.Flags().StringVarP(&cliConfig.URL, "url", "u", "", "URL for stress testing") + rootCmd.Flags().IntVarP(&dodosCount, "dodos-count", "d", config.DefaultDodosCount, "Number of dodos(threads)") + rootCmd.Flags().IntVarP(&requestCount, "request-count", "r", config.DefaultRequestCount, "Number of total requests") + rootCmd.Flags().IntVarP(&timeout, "timeout", "t", config.DefaultTimeout, "Timeout for each request in milliseconds") + if err := rootCmd.Execute(); err != nil { + utils.PrintErr(err) + rootCmd.Println(rootCmd.UsageString()) + return nil, customerrors.CobraErrorFormater(err) + } + rootCmd.Flags().Visit(func(f *pflag.Flag) { + switch f.Name { + case "help": + returnNil = true + case "version": + returnNil = true + case "dodos-count": + cliConfig.DodosCount = dodosCount + case "request-count": + cliConfig.RequestCount = requestCount + case "timeout": + cliConfig.Timeout = timeout + } + }) + if returnNil { + return nil, nil + } + return cliConfig, nil +} + +func CLIYesOrNoReader(message string) bool { + var answer string + fmt.Printf("%s [y/N]: ", message) + if _, err := fmt.Scanln(&answer); err != nil { + return false + } + return answer == "y" || answer == "Y" +} diff --git a/readers/json.go b/readers/json.go new file mode 100644 index 0000000..cf8725e --- /dev/null +++ b/readers/json.go @@ -0,0 +1,33 @@ +package readers + +import ( + "encoding/json" + "os" + + "github.com/aykhans/dodo/config" + "github.com/aykhans/dodo/custom_errors" +) + +func JSONConfigReader(filePath string) (*config.JSONConfig, error) { + data, err := os.ReadFile(filePath) + if err != nil { + return nil, customerrors.OSErrorFormater(err) + } + jsonConf := &config.JSONConfig{} + err = json.Unmarshal(data, &jsonConf) + + if err != nil { + switch err := err.(type) { + case *json.UnmarshalTypeError: + return nil, + customerrors.NewTypeError( + err.Type.String(), + err.Value, + err.Field, + err, + ) + } + return nil, customerrors.NewInvalidFileError(filePath, err) + } + return jsonConf, nil +} diff --git a/requests/requests.go b/requests/requests.go new file mode 100644 index 0000000..1ca6c82 --- /dev/null +++ b/requests/requests.go @@ -0,0 +1,402 @@ +package requests + +import ( + "fmt" + "io" + "math/rand" + "net/http" + "net/url" + "strings" + + "sync" + "time" + + "github.com/aykhans/dodo/config" + customerrors "github.com/aykhans/dodo/custom_errors" + "github.com/aykhans/dodo/readers" + "github.com/aykhans/dodo/utils" + "github.com/jedib0t/go-pretty/v6/progress" +) + +type DodoResponse struct { + Response string + Time time.Duration +} + +type DodoResponses []DodoResponse + +type MergedDodoResponse struct { + Response string + Count int + AvgTime time.Duration + MinTime time.Duration + MaxTime time.Duration +} + +func (d DodoResponses) Len() int { + return len(d) +} + +func (d DodoResponses) MinTime() time.Duration { + minTime := d[0].Time + for _, response := range d { + if response.Time < minTime { + minTime = response.Time + } + } + return minTime +} + +func (d DodoResponses) MaxTime() time.Duration { + maxTime := d[0].Time + for _, response := range d { + if response.Time > maxTime { + maxTime = response.Time + } + } + return maxTime +} + +func (d DodoResponses) AvgTime() time.Duration { + var sum time.Duration + for _, response := range d { + sum += response.Time + } + return sum / time.Duration(len(d)) +} + +func (d DodoResponses) MergeDodoResponses() []MergedDodoResponse { + mergedResponses := make(map[string]*struct { + count int + minTime time.Duration + maxTime time.Duration + totalTime time.Duration + }) + for _, response := range d { + if _, ok := mergedResponses[response.Response]; !ok { + mergedResponses[response.Response] = &struct { + count int + minTime time.Duration + maxTime time.Duration + totalTime time.Duration + }{ + count: 1, + minTime: response.Time, + maxTime: response.Time, + totalTime: response.Time, + } + } else { + mergedResponses[response.Response].count++ + mergedResponses[response.Response].totalTime += response.Time + if response.Time < mergedResponses[response.Response].minTime { + mergedResponses[response.Response].minTime = response.Time + } + if response.Time > mergedResponses[response.Response].maxTime { + mergedResponses[response.Response].maxTime = response.Time + } + + } + } + var result []MergedDodoResponse + for response, data := range mergedResponses { + result = append(result, MergedDodoResponse{ + Response: response, + Count: data.count, + AvgTime: data.totalTime / time.Duration(data.count), + MinTime: data.minTime, + MaxTime: data.maxTime, + }) + } + return result +} + +func Run(conf *config.DodoConfig) (DodoResponses, error) { + params := setParams(conf.URL, conf.Params) + headers := getHeaders(conf.Headers) + + dodosCountForRequest, dodosCountForProxies := conf.DodosCount, conf.DodosCount + if dodosCountForRequest > conf.RequestCount { + dodosCountForRequest = conf.RequestCount + } + proxiesCount := len(conf.Proxies) + if dodosCountForProxies > proxiesCount { + dodosCountForProxies = proxiesCount + } + dodosCountForProxies = min(dodosCountForProxies, config.MaxDodosCountForProxies) + + var wg sync.WaitGroup + wg.Add(dodosCountForRequest + 1) + var requestCountPerDodo int + responses := make([][]DodoResponse, dodosCountForRequest) + getClient := getClientFunc(conf.Proxies, conf.Timeout, dodosCountForProxies) + + countSlice := make([]int, dodosCountForRequest) + go printProgress(&wg, conf.RequestCount, "Dodos Working🔥", &countSlice) + + for i := 0; i < dodosCountForRequest; i++ { + if i+1 == dodosCountForRequest { + requestCountPerDodo = conf.RequestCount - + (i * conf.RequestCount / dodosCountForRequest) + } else { + requestCountPerDodo = ((i + 1) * conf.RequestCount / dodosCountForRequest) - + (i * conf.RequestCount / dodosCountForRequest) + } + go sendRequest( + &responses[i], + &countSlice[i], + requestCountPerDodo, + conf.Method, + params, + conf.Body, + headers, + conf.Cookies, + getClient, + &wg, + ) + } + wg.Wait() + return utils.Flatten(responses), nil +} + +func sendRequest( + responseData *[]DodoResponse, + counter *int, + requestCout int, + method string, + params string, + body string, + headers http.Header, + cookies map[string]string, + getClient func() http.Client, + wg *sync.WaitGroup, +) { + defer wg.Done() + for j := 0; j < requestCout; j++ { + func() { + defer func() { *counter++ }() + req, _ := http.NewRequest( + method, + params, + getBodyReader(body), + ) + // req.Header.Set("User-Agent", config) + req.Header = headers + setCookies(req, cookies) + client := getClient() + startTime := time.Now() + resp, err := client.Do(req) + completedTime := time.Since(startTime) + if err != nil { + *responseData = append( + *responseData, + DodoResponse{ + Response: customerrors.RequestErrorsFormater(err), + Time: completedTime, + }, + ) + return + } + defer resp.Body.Close() + *responseData = append( + *responseData, + DodoResponse{ + Response: resp.Status, + Time: completedTime, + }, + ) + }() + } +} + +func setCookies(req *http.Request, cookies map[string]string) { + for key, value := range cookies { + req.AddCookie(&http.Cookie{Name: key, Value: value}) + } +} + +func getHeaders(headers map[string]string) http.Header { + httpHeaders := make(http.Header, len(headers)) + httpHeaders.Set("User-Agent", config.DefaultUserAgent) + for key, value := range headers { + httpHeaders.Add(key, value) + } + return httpHeaders +} + +func getBodyReader(bodyString string) io.Reader { + if bodyString == "" { + return http.NoBody + } + return strings.NewReader(bodyString) + +} + +func setParams(baseURL string, params map[string]string) string { + if len(params) == 0 { + return baseURL + } + urlParams := url.Values{} + for key, value := range params { + urlParams.Add(key, value) + } + baseURLWithParams := fmt.Sprintf("%s?%s", baseURL, urlParams.Encode()) + return baseURLWithParams +} + +func printProgress(wg *sync.WaitGroup, total int, message string, countSlice *[]int) { + defer wg.Done() + pw := progress.NewWriter() + pw.SetTrackerPosition(progress.PositionRight) + pw.SetStyle(progress.StyleBlocks) + pw.SetTrackerLength(40) + pw.SetUpdateFrequency(time.Millisecond * 250) + go pw.Render() + dodosTracker := progress.Tracker{Message: message, Total: int64(total)} + pw.AppendTracker(&dodosTracker) + for { + totalCount := 0 + for _, count := range *countSlice { + // println(count) + totalCount += count + } + // println(totalCount) + dodosTracker.SetValue(int64(totalCount)) + if totalCount == total { + break + } + time.Sleep(time.Millisecond * 200) + } + dodosTracker.MarkAsDone() + time.Sleep(time.Millisecond * 300) + pw.Stop() +} + +func getClientFunc(proxies config.ProxySlice, timeout time.Duration, dodosCount int) func() http.Client { + if len(proxies) > 0 { + activeProxyClientsArray := make([][]http.Client, dodosCount) + proxiesCount := len(proxies) + var wg sync.WaitGroup + wg.Add(dodosCount + 1) + var proxiesSlice config.ProxySlice + + countSlice := make([]int, dodosCount) + go printProgress(&wg, proxiesCount, "Searching for active proxies🌐", &countSlice) + + for i := 0; i < dodosCount; i++ { + if i+1 == dodosCount { + proxiesSlice = proxies[i*proxiesCount/dodosCount:] + } else { + proxiesSlice = proxies[i*proxiesCount/dodosCount : (i+1)*proxiesCount/dodosCount] + } + go findActiveProxyClients( + proxiesSlice, + timeout, + &activeProxyClientsArray[i], + &countSlice[i], + &wg, + ) + } + wg.Wait() + + activeProxyClients := utils.Flatten(activeProxyClientsArray) + activeProxyClientsCount := len(activeProxyClients) + var yesOrNoMessage string + if activeProxyClientsCount == 0 { + yesOrNoMessage = utils.Colored( + utils.Colors.Red, + "No active proxies found. Do you want to continue?", + ) + } else { + yesOrNoMessage = utils.Colored( + utils.Colors.Yellow, + fmt.Sprintf("Found %d active proxies. Do you want to continue?", activeProxyClientsCount), + ) + } + fmt.Println() + proceed := readers.CLIYesOrNoReader(yesOrNoMessage) + if !proceed { + utils.PrintAndExit("Exiting...") + } + fmt.Println() + if activeProxyClientsCount == 0 { + return func() http.Client { + return getNewClient(timeout) + } + } + return func() http.Client { + return getRandomClient(activeProxyClients, activeProxyClientsCount) + } + } + return func() http.Client { + return getNewClient(timeout) + } +} + +func findActiveProxyClients( + proxies config.ProxySlice, + timeout time.Duration, + activeProxyClients *[]http.Client, + counter *int, + wg *sync.WaitGroup) { + defer wg.Done() + for _, proxy := range proxies { + func() { + defer func() { *counter++ }() + transport, err := getTransport(proxy) + if err != nil { + return + } + client := &http.Client{ + Transport: transport, + Timeout: timeout, + } + resp, err := client.Get(config.ProxyCheckURL) + if err != nil { + return + } + defer resp.Body.Close() + if resp.StatusCode == 200 { + *activeProxyClients = append( + *activeProxyClients, + http.Client{ + Transport: transport, + Timeout: timeout, + }, + ) + } + }() + } +} + +func getTransport(proxy map[string]string) (*http.Transport, error) { + proxyURL, err := url.Parse(proxy["url"]) + if err != nil { + return nil, err + } + if _, ok := proxy["username"]; !ok { + transport := &http.Transport{ + Proxy: http.ProxyURL(proxyURL), + } + return transport, nil + } + + transport := &http.Transport{ + Proxy: http.ProxyURL( + &url.URL{ + Scheme: proxyURL.Scheme, + Host: proxyURL.Host, + User: url.UserPassword(proxy["username"], proxy["password"]), + }, + ), + } + return transport, nil +} + +func getRandomClient(clients []http.Client, clientsCount int) http.Client { + randomIndex := rand.Intn(clientsCount) + return clients[randomIndex] +} + +func getNewClient(timeout time.Duration) http.Client { + return http.Client{Timeout: timeout} +} diff --git a/utils/print.go b/utils/print.go new file mode 100644 index 0000000..505178c --- /dev/null +++ b/utils/print.go @@ -0,0 +1,60 @@ +package utils + +import ( + "fmt" + "os" +) + +type colors struct { + reset string + Red string + Green string + Yellow string + Orange string + Blue string + Magenta string + Cyan string + Gray string + White string +} + +var Colors = colors{ + reset: "\033[0m", + Red: "\033[31m", + Green: "\033[32m", + Yellow: "\033[33m", + Orange: "\033[38;5;208m", + Blue: "\033[34m", + Magenta: "\033[35m", + Cyan: "\033[36m", + Gray: "\033[37m", + White: "\033[97m", +} + +func Colored(color string, a ...any) string { + return color + fmt.Sprint(a...) + Colors.reset +} + +func PrintfC(color string, format string, a ...any) { + fmt.Printf(Colored(color, format), a...) +} + +func PrintlnC(color string, a ...any) { + fmt.Println(Colored(color, a...)) +} + +func PrintErr(err error) { + PrintlnC(Colors.Red, err.Error()) +} + +func PrintErrAndExit(err error) { + if err != nil { + PrintErr(err) + os.Exit(0) + } +} + +func PrintAndExit(message string) { + fmt.Println(message) + os.Exit(0) +} diff --git a/utils/slice.go b/utils/slice.go new file mode 100644 index 0000000..e329604 --- /dev/null +++ b/utils/slice.go @@ -0,0 +1,18 @@ +package utils + +func Flatten[T any](nested [][]T) []T { + flattened := make([]T, 0) + for _, n := range nested { + flattened = append(flattened, n...) + } + return flattened +} + +func Contains[T comparable](slice []T, item T) bool { + for _, i := range slice { + if i == item { + return true + } + } + return false +} diff --git a/validation/validator.go b/validation/validator.go new file mode 100644 index 0000000..da27b9f --- /dev/null +++ b/validation/validator.go @@ -0,0 +1,57 @@ +package validation + +import ( + "reflect" + "strings" + + "github.com/aykhans/dodo/config" + "github.com/go-playground/validator/v10" + "golang.org/x/net/http/httpguts" +) + +// net/http/request.go/isNotToken +func isNotToken(r rune) bool { + return !httpguts.IsTokenRune(r) +} + +func NewValidator() *validator.Validate { + validation := validator.New() + validation.RegisterTagNameFunc(func(fld reflect.StructField) string { + if fld.Tag.Get("validation_name") != "" { + return fld.Tag.Get("validation_name") + } else { + return fld.Tag.Get("json") + } + }) + validation.RegisterValidation( + "http_method", + func(fl validator.FieldLevel) bool { + method := fl.Field().String() + // net/http/request.go/validMethod + return len(method) > 0 && strings.IndexFunc(method, isNotToken) == -1 + }, + ) + validation.RegisterValidation( + "string_bool", + func(fl validator.FieldLevel) bool { + s := fl.Field().String() + return s == "true" || s == "false" || s == "" + }, + ) + validation.RegisterValidation( + "url_map_slice", + func(fl validator.FieldLevel) bool { + proxies := fl.Field().Interface().(config.ProxySlice) + for _, proxy := range proxies { + if _, ok := proxy["url"]; !ok { + return false + } + if err := validation.Var(proxy["url"], "url"); err != nil { + return false + } + } + return true + }, + ) + return validation +}