mirror of
https://github.com/aykhans/dodo.git
synced 2025-04-18 18:39:43 +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