mirror of
https://github.com/aykhans/dodo.git
synced 2025-04-20 11:11:26 +00:00
🎉first commit
This commit is contained in:
commit
7a2558b25a
1
.gitignore
vendored
Normal file
1
.gitignore
vendored
Normal file
@ -0,0 +1 @@
|
|||||||
|
dodo
|
21
config.json
Normal file
21
config.json
Normal 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
140
config/config.go
Normal 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
114
custom_errors/errors.go
Normal 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
|
||||||
|
}
|
76
custom_errors/formaters.go
Normal file
76
custom_errors/formaters.go
Normal 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
25
go.mod
Normal 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
45
go.sum
Normal 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
106
main.go
Normal 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
85
readers/cli.go
Normal 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
33
readers/json.go
Normal 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
402
requests/requests.go
Normal 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
60
utils/print.go
Normal 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
18
utils/slice.go
Normal 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
57
validation/validator.go
Normal 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
|
||||||
|
}
|
Loading…
x
Reference in New Issue
Block a user