🎉first commit

This commit is contained in:
Aykhan Shahsuvarov 2024-05-25 20:26:20 +04:00
commit 7a2558b25a
15 changed files with 1183 additions and 0 deletions

1
.gitignore vendored Normal file
View File

@ -0,0 +1 @@
dodo

0
README.md Normal file
View File

21
config.json Normal file
View File

@ -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"
}
]
}

140
config/config.go Normal file
View File

@ -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
}
}

114
custom_errors/errors.go Normal file
View File

@ -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
}

View File

@ -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"
}

25
go.mod Normal file
View File

@ -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
)

45
go.sum Normal file
View File

@ -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=

106
main.go Normal file
View File

@ -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()
}

85
readers/cli.go Normal file
View File

@ -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"
}

33
readers/json.go Normal file
View File

@ -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
}

402
requests/requests.go Normal file
View File

@ -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}
}

60
utils/print.go Normal file
View File

@ -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)
}

18
utils/slice.go Normal file
View File

@ -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
}

57
validation/validator.go Normal file
View File

@ -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
}