1 Commits

Author SHA1 Message Date
2bd45e22a4 Merge a468f663bf into 565079fe97 2024-11-23 12:29:17 +00:00
43 changed files with 1407 additions and 1691 deletions

View File

@ -4,7 +4,6 @@ binaries
dodo dodo
.git .git
.gitignore .gitignore
.golangci.yml
README.md README.md
LICENSE LICENSE
config.json config.json

View File

@ -1,25 +0,0 @@
name: golangci-lint
on:
push:
branches:
- main
pull_request:
permissions:
contents: read
jobs:
golangci:
name: lint
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- uses: actions/setup-go@v5
with:
go-version: stable
- name: golangci-lint
uses: golangci/golangci-lint-action@v6
with:
version: v1.64
args: --timeout=10m --config=.golangci.yml

View File

@ -1,27 +0,0 @@
run:
go: "1.24"
concurrency: 8
timeout: 10m
linters:
disable-all: true
enable:
- asasalint
- asciicheck
- errcheck
- gofmt
- goimports
- gomodguard
- goprintffuncname
- govet
- ineffassign
- misspell
- nakedret
- nolintlint
- prealloc
- reassign
- staticcheck
- typecheck
- unconvert
- unused
- whitespace

View File

@ -1,6 +1,6 @@
FROM golang:1.24-alpine AS builder FROM golang:1.22.6-alpine AS builder
WORKDIR /src WORKDIR /dodo
COPY go.mod go.sum ./ COPY go.mod go.sum ./
RUN go mod download RUN go mod download
@ -11,9 +11,9 @@ RUN echo "{}" > config.json
FROM gcr.io/distroless/static-debian12:latest FROM gcr.io/distroless/static-debian12:latest
WORKDIR / WORKDIR /dodo
COPY --from=builder /src/dodo /dodo COPY --from=builder /dodo/dodo /dodo/dodo
COPY --from=builder /src/config.json /config.json COPY --from=builder /dodo/config.json /dodo/config.json
ENTRYPOINT ["./dodo", "-f", "/config.json"] ENTRYPOINT ["./dodo", "-c", "/dodo/config.json"]

View File

@ -1,9 +0,0 @@
lint:
golangci-lint run
build:
go build -ldflags "-s -w" -o "./dodo"
build-all:
rm -rf ./binaries
./build.sh

305
README.md
View File

@ -1,186 +1,119 @@
<h1 align="center">Dodo - A Fast and Easy-to-Use HTTP Benchmarking Tool</h1> <h1 align="center">Dodo is a simple and easy-to-use HTTP benchmarking tool.</h1>
<p align="center"> <p align="center">
<img width="30%" height="30%" src="https://ftp.aykhans.me/web/client/pubshares/VzPtSHS7yPQT7ngoZzZSNU/browse?path=%2Fdodo.png"> <img width="30%" height="30%" src="https://raw.githubusercontent.com/aykhans/dodo/main/assets/dodo.png">
</p> </p>
## Installation ## Installation
### With Docker (Recommended)
### Using Docker (Recommended) Pull the Dodo image from Docker Hub:
```sh
Pull the Dodo image from Docker Hub: docker pull aykhans/dodo:latest
```
```sh If you use Dodo with Docker and a config file, you must provide the config.json file as a volume to the Docker run command (not as the "-c config.json" argument), as shown in the examples in the [usage](#usage) section.
docker pull aykhans/dodo:latest
``` ### With Binary File
You can grab binaries in the [releases](https://github.com/aykhans/dodo/releases) section.
When using Dodo with Docker and a local config file, you must provide the config.json file as a volume to the Docker run command (not as the "-f config.json" argument):
### Build from Source
```sh To build Dodo from source, you need to have [Go1.22+](https://golang.org/dl/) installed. <br>
docker run -v /path/to/config.json:/config.json aykhans/dodo Follow the steps below to build dodo:
```
1. **Clone the repository:**
If you're using Dodo with Docker and providing a config file via URL, you don't need to set a volume:
```sh
```sh git clone https://github.com/aykhans/dodo.git
docker run aykhans/dodo -f https://raw.githubusercontent.com/aykhans/dodo/main/config.json ```
```
2. **Navigate to the project directory:**
### Using Binary Files
```sh
You can download pre-built binaries from the [releases](https://github.com/aykhans/dodo/releases) section. cd dodo
```
### Building from Source
3. **Build the project:**
To build Dodo from source, you need to have [Go 1.24+](https://golang.org/dl/) installed.
Follow these steps: ```sh
go build -ldflags "-s -w" -o dodo
1. **Clone the repository:** ```
```sh This will generate an executable named `dodo` in the project directory.
git clone https://github.com/aykhans/dodo.git
``` ## Usage
You can use Dodo with CLI arguments, a JSON config file, or both. If you use both, CLI arguments will always override JSON config arguments if there is a conflict.
2. **Navigate to the project directory:**
### 1. CLI
```sh Send 1000 GET requests to https://example.com with 10 parallel dodos (threads) and a timeout of 2000 milliseconds:
cd dodo
``` ```sh
dodo -u https://example.com -m GET -d 10 -r 1000 -t 2000
3. **Build the project:** ```
With Docker:
```sh ```sh
go build -ldflags "-s -w" -o dodo docker run --rm aykhans/dodo -u https://example.com -m GET -d 10 -r 1000 -t 2000
``` ```
This will generate an executable named `dodo` in the project directory. ### 2. JSON config file
You can find an example config structure in the [config.json](https://github.com/aykhans/dodo/blob/main/config.json) file:
## Usage ```json
{
You can use Dodo with CLI arguments, a JSON config file, or both. When using both, CLI arguments will override JSON config values if there's a conflict. "method": "GET",
"url": "https://example.com",
### 1. CLI "no_proxy_check": false,
"timeout": 2000,
Send 1000 GET requests to https://example.com with 10 parallel dodos (threads) and a timeout of 2 seconds: "dodos_count": 10,
"request_count": 1000,
```sh "params": {},
dodo -u https://example.com -m GET -d 10 -r 1000 -t 2s "headers": {},
``` "cookies": {},
"body": [""],
With Docker: "proxies": [
{
```sh "url": "http://example.com:8080",
docker run --rm -i aykhans/dodo -u https://example.com -m GET -d 10 -r 1000 -t 2s "username": "username",
``` "password": "password"
},
### 2. JSON Config File {
"url": "http://example.com:8080"
Send 1000 GET requests to https://example.com with 10 parallel dodos (threads) and a timeout of 800 milliseconds: }
]
```jsonc }
{ ```
"method": "GET", Send 1000 GET requests to https://example.com with 10 parallel dodos (threads) and a timeout of 2000 milliseconds:
"url": "https://example.com",
"yes": false, ```sh
"timeout": "800ms", dodo -c /path/config.json
"dodos": 10, ```
"requests": 1000, With Docker:
```sh
"params": [ docker run --rm -v ./path/config.json:/dodo/config.json -i aykhans/dodo
// A random value will be selected from the list for first "key1" param on each request ```
// And always "value" for second "key1" param on each request
// e.g. "?key1=value2&key1=value" ### 3. Both (CLI & JSON)
{ "key1": ["value1", "value2", "value3", "value4"] }, Override the config file arguments with CLI arguments:
{ "key1": "value" },
```sh
// A random value will be selected from the list for param "key2" on each request dodo -c /path/config.json -u https://example.com -m GET -d 10 -r 1000 -t 2000
// e.g. "?key2=value2" ```
{ "key2": ["value1", "value2"] }, With Docker:
], ```sh
docker run --rm -v ./path/config.json:/dodo/config.json -i aykhans/dodo -u https://example.com -m GET -d 10 -r 1000 -t 2000
"headers": [ ```
// A random value will be selected from the list for first "key1" header on each request
// And always "value" for second "key1" header on each request ## CLI and JSON Config Parameters
// e.g. "key1: value3", "key1: value" If the Headers, Params, Cookies and Body fields have multiple values, each request will choose a random value from the list.
{ "key1": ["value1", "value2", "value3", "value4"] },
{ "key1": "value" }, | Parameter | JSON config file | CLI Flag | CLI Short Flag | Type | Description | Default |
| --------------------- | ---------------- | --------------- | -------------- | -------------------------------- | ------------------------------------------------------------------- | ----------- |
// A random value will be selected from the list for header "key2" on each request | Config file | - | --config-file | -c | String | Path to the JSON config file | - |
// e.g. "key2: value2" | Yes | - | --yes | -y | Boolean | Answer yes to all questions | false |
{ "key2": ["value1", "value2"] }, | URL | url | --url | -u | String | URL to send the request to | - |
], | Method | method | --method | -m | String | HTTP method | GET |
| Request count | request_count | --request-count | -r | Integer | Total number of requests to send | 1000 |
"cookies": [ | Dodos count (Threads) | dodos_count | --dodos-count | -d | Integer | Number of dodos (threads) to send requests in parallel | 1 |
// A random value will be selected from the list for first "key1" cookie on each request | Timeout | timeout | --timeout | -t | Integer | Timeout for canceling each request (milliseconds) | 10000 |
// And always "value" for second "key1" cookie on each request | No Proxy Check | no_proxy_check | --no-proxy-check| - | Boolean | Disable proxy check | false |
// e.g. "key1=value4; key1=value" | Params | params | - | - | Key-Value {String: [String]} | Request parameters | - |
{ "key1": ["value1", "value2", "value3", "value4"] }, | Headers | headers | - | - | Key-Value {String: [String]} | Request headers | - |
{ "key1": "value" }, | Cookies | cookies | - | - | Key-Value {String: [String]} | Request cookies | - |
| Body | body | - | - | [String] | Request body | - |
// A random value will be selected from the list for cookie "key2" on each request | Proxy | proxies | - | - | List[Key-Value {string: string}] | List of proxies (will check active proxies before sending requests) | - |
// e.g. "key2=value1"
{ "key2": ["value1", "value2"] },
],
"body": "body-text",
// OR
// A random body value will be selected from the list for each request
"body": ["body-text1", "body-text2", "body-text3"],
"proxy": "http://example.com:8080",
// OR
// A random proxy will be selected from the list for each request
"proxy": [
"http://example.com:8080",
"http://username:password@example.com:8080",
"socks5://example.com:8080",
"socks5h://example.com:8080",
],
}
```
```sh
dodo -f /path/config.json
# OR
dodo -f https://example.com/config.json
```
With Docker:
```sh
docker run --rm -i -v /path/to/config.json:/config.json aykhans/dodo
# OR
docker run --rm -i aykhans/dodo -f https://example.com/config.json
```
### 3. Combined (CLI & JSON)
Override the config file arguments with CLI arguments:
```sh
dodo -f /path/to/config.json -u https://example.com -m GET -d 10 -r 1000 -t 5s
```
With Docker:
```sh
docker run --rm -i -v /path/to/config.json:/config.json aykhans/dodo -u https://example.com -m GET -d 10 -r 1000 -t 5s
```
## CLI and JSON Config Parameters
If `Headers`, `Params`, `Cookies`, `Body`, or `Proxy` fields have multiple values, each request will choose a random value from the list.
| Parameter | JSON config file | CLI Flag | CLI Short Flag | Type | Description | Default |
| --------------- | ---------------- | ------------ | -------------- | ------------------------------ | --------------------------------------------------------------- | ------- |
| Config file | - | -config-file | -f | String | Path to local config file or http(s) URL of the config file | - |
| Yes | yes | -yes | -y | Boolean | Answer yes to all questions | false |
| URL | url | -url | -u | String | URL to send the request to | - |
| Method | method | -method | -m | String | HTTP method | GET |
| Requests | requests | -requests | -r | UnsignedInteger | Total number of requests to send | 1000 |
| Dodos (Threads) | dodos | -dodos | -d | UnsignedInteger | Number of dodos (threads) to send requests in parallel | 1 |
| Timeout | timeout | -timeout | -t | Duration | Timeout for canceling each request | 10s |
| Params | params | -param | -p | [{String: String OR [String]}] | Request parameters | - |
| Headers | headers | -header | -H | [{String: String OR [String]}] | Request headers | - |
| Cookies | cookies | -cookie | -c | [{String: String OR [String]}] | Request cookies | - |
| Body | body | -body | -b | String OR [String] | Request body or list of request bodies | - |
| Proxy | proxies | -proxy | -x | String OR [String] | Proxy URL or list of proxy URLs | - |

BIN
assets/dodo.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 230 KiB

View File

@ -1,35 +1,22 @@
{ {
"method": "GET", "method": "GET",
"url": "https://example.com", "url": "https://example.com",
"yes": false, "no_proxy_check": false,
"timeout": "5s", "timeout": 10000,
"dodos": 8, "dodos_count": 50,
"requests": 1000, "request_count": 1000,
"params": {},
"params": [ "headers": {},
{ "key1": ["value1", "value2", "value3", "value4"] }, "cookies": {},
{ "key1": "value" }, "body": [""],
{ "key2": ["value1", "value2"] } "proxies": [
], {
"url": "http://example.com:8080",
"headers": [ "username": "username",
{ "key1": ["value1", "value2", "value3", "value4"] }, "password": "password"
{ "key1": "value" }, },
{ "key2": ["value1", "value2"] } {
], "url": "http://example.com:8080"
}
"cookies": [
{ "key1": ["value1", "value2", "value3", "value4"] },
{ "key1": "value" },
{ "key2": ["value1", "value2"] }
],
"body": ["body-text1", "body-text2", "body-text3"],
"proxy": [
"http://example.com:8080",
"http://username:password@example.com:8080",
"socks5://example.com:8080",
"socks5h://example.com:8080"
] ]
} }

View File

@ -1,39 +0,0 @@
# YAML/YML config file option is not implemented yet.
# This file is a example for future implementation.
method: "GET"
url: "https://example.com"
yes: false
timeout: "5s"
dodos: 10
requests: 1000
params:
- key1: ["value1", "value2", "value3", "value4"]
- key1: "value"
- key2: ["value1", "value2"]
headers:
- key1: ["value1", "value2", "value3", "value4"]
- key1: "value"
- key2: ["value1", "value2"]
cookies:
- key1: ["value1", "value2", "value3", "value4"]
- key1: "value"
- key2: ["value1", "value2"]
# body: "body-text"
# OR
body:
- "body-text1"
- "body-text2"
- "body-text3"
# proxy: "http://example.com:8080"
# OR
proxy:
- "http://example.com:8080"
- "http://username:password@example.com:8080"
- "socks5://example.com:8080"
- "socks5h://example.com:8080"

View File

@ -1,175 +0,0 @@
package config
import (
"flag"
"fmt"
"os"
"strings"
"time"
"github.com/aykhans/dodo/types"
"github.com/aykhans/dodo/utils"
)
const cliUsageText = `Usage:
dodo [flags]
Examples:
Simple usage only with URL:
dodo -u https://example.com
Usage with config file:
dodo -f /path/to/config/file/config.json
Usage with all flags:
dodo -f /path/to/config/file/config.json \
-u https://example.com -m POST \
-d 10 -r 1000 -t 3s \
-b "body1" -body "body2" \
-H "header1:value1" -header "header2:value2" \
-p "param1=value1" -param "param2=value2" \
-c "cookie1=value1" -cookie "cookie2=value2" \
-x "http://proxy.example.com:8080" -proxy "socks5://proxy2.example.com:8080" \
-y
Flags:
-h, -help help for dodo
-v, -version version for dodo
-y, -yes bool Answer yes to all questions (default %v)
-f, -config-file string Path to the local config file or http(s) URL of the config file
-d, -dodos uint Number of dodos(threads) (default %d)
-r, -requests uint Number of total requests (default %d)
-t, -timeout Duration Timeout for each request (e.g. 400ms, 15s, 1m10s) (default %v)
-u, -url string URL for stress testing
-m, -method string HTTP Method for the request (default %s)
-b, -body [string] Body for the request (e.g. "body text")
-p, -param [string] Parameter for the request (e.g. "key1=value1")
-H, -header [string] Header for the request (e.g. "key1: value1")
-c, -cookie [string] Cookie for the request (e.g. "key1=value1")
-x, -proxy [string] Proxy for the request (e.g. "http://proxy.example.com:8080")`
func (config *Config) ReadCLI() (types.ConfigFile, error) {
flag.Usage = func() {
fmt.Printf(
cliUsageText+"\n",
DefaultYes,
DefaultDodosCount,
DefaultRequestCount,
DefaultTimeout,
DefaultMethod,
)
}
var (
version = false
configFile = ""
yes = false
method = ""
url types.RequestURL
dodosCount = uint(0)
requestCount = uint(0)
timeout time.Duration
)
{
flag.BoolVar(&version, "version", false, "Prints the version of the program")
flag.BoolVar(&version, "v", false, "Prints the version of the program")
flag.StringVar(&configFile, "config-file", "", "Path to the configuration file")
flag.StringVar(&configFile, "f", "", "Path to the configuration file")
flag.BoolVar(&yes, "yes", false, "Answer yes to all questions")
flag.BoolVar(&yes, "y", false, "Answer yes to all questions")
flag.StringVar(&method, "method", "", "HTTP Method")
flag.StringVar(&method, "m", "", "HTTP Method")
flag.Var(&url, "url", "URL to send the request")
flag.Var(&url, "u", "URL to send the request")
flag.UintVar(&dodosCount, "dodos", 0, "Number of dodos(threads)")
flag.UintVar(&dodosCount, "d", 0, "Number of dodos(threads)")
flag.UintVar(&requestCount, "requests", 0, "Number of total requests")
flag.UintVar(&requestCount, "r", 0, "Number of total requests")
flag.DurationVar(&timeout, "timeout", 0, "Timeout for each request (e.g. 400ms, 15s, 1m10s)")
flag.DurationVar(&timeout, "t", 0, "Timeout for each request (e.g. 400ms, 15s, 1m10s)")
flag.Var(&config.Params, "param", "URL parameter to send with the request")
flag.Var(&config.Params, "p", "URL parameter to send with the request")
flag.Var(&config.Headers, "header", "Header to send with the request")
flag.Var(&config.Headers, "H", "Header to send with the request")
flag.Var(&config.Cookies, "cookie", "Cookie to send with the request")
flag.Var(&config.Cookies, "c", "Cookie to send with the request")
flag.Var(&config.Body, "body", "Body to send with the request")
flag.Var(&config.Body, "b", "Body to send with the request")
flag.Var(&config.Proxies, "proxy", "Proxy to use for the request")
flag.Var(&config.Proxies, "x", "Proxy to use for the request")
}
flag.Parse()
if len(os.Args) <= 1 {
flag.CommandLine.Usage()
os.Exit(0)
}
if args := flag.Args(); len(args) > 0 {
return types.ConfigFile(configFile), fmt.Errorf("unexpected arguments: %v", strings.Join(args, ", "))
}
if version {
fmt.Printf("dodo version %s\n", VERSION)
os.Exit(0)
}
flag.Visit(func(f *flag.Flag) {
switch f.Name {
case "method", "m":
config.Method = utils.ToPtr(method)
case "url", "u":
config.URL = utils.ToPtr(url)
case "dodos", "d":
config.DodosCount = utils.ToPtr(dodosCount)
case "requests", "r":
config.RequestCount = utils.ToPtr(requestCount)
case "timeout", "t":
config.Timeout = &types.Timeout{Duration: timeout}
case "yes", "y":
config.Yes = utils.ToPtr(yes)
}
})
return types.ConfigFile(configFile), nil
}
// CLIYesOrNoReader reads a yes or no answer from the command line.
// It prompts the user with the given message and default value,
// and returns true if the user answers "y" or "Y", and false otherwise.
// If there is an error while reading the input, it returns false.
// If the user simply presses enter without providing any input,
// it returns the default value specified by the `dft` parameter.
func CLIYesOrNoReader(message string, dft bool) bool {
var answer string
defaultMessage := "Y/n"
if !dft {
defaultMessage = "y/N"
}
fmt.Printf("%s [%s]: ", message, defaultMessage)
if _, err := fmt.Scanln(&answer); err != nil {
if err.Error() == "unexpected newline" {
return dft
}
return false
}
if answer == "" {
return dft
}
return answer == "y" || answer == "Y"
}

View File

@ -1,203 +1,159 @@
package config package config
import ( import (
"errors"
"fmt" "fmt"
"net/url" "net/url"
"os" "os"
"slices"
"strings"
"time" "time"
"github.com/aykhans/dodo/types"
"github.com/aykhans/dodo/utils" "github.com/aykhans/dodo/utils"
"github.com/jedib0t/go-pretty/v6/table" "github.com/jedib0t/go-pretty/v6/table"
) )
const ( const (
VERSION string = "0.6.0" VERSION string = "0.5.2"
DefaultUserAgent string = "Dodo/" + VERSION DefaultUserAgent string = "Dodo/" + VERSION
DefaultMethod string = "GET" ProxyCheckURL string = "https://www.google.com"
DefaultTimeout time.Duration = time.Second * 10 DefaultMethod string = "GET"
DefaultDodosCount uint = 1 DefaultTimeout uint32 = 10000 // Milliseconds (10 seconds)
DefaultRequestCount uint = 1 DefaultDodosCount uint = 1
DefaultYes bool = false DefaultRequestCount uint = 1000
MaxDodosCountForProxies uint = 20 // Max dodos count for proxy check
) )
var SupportedProxySchemes []string = []string{"http", "socks5", "socks5h"} type IConfig interface {
MergeConfigs(newConfig IConfig) IConfig
}
type RequestConfig struct { type RequestConfig struct {
Method string `json:"method"` Method string
URL url.URL `json:"url"` URL *url.URL
Timeout time.Duration `json:"timeout"` Timeout time.Duration
DodosCount uint `json:"dodos"` DodosCount uint
RequestCount uint `json:"requests"` RequestCount uint
Yes bool `json:"yes"` Params map[string][]string
Params types.Params `json:"params"` Headers map[string][]string
Headers types.Headers `json:"headers"` Cookies map[string][]string
Cookies types.Cookies `json:"cookies"` Proxies []Proxy
Body types.Body `json:"body"` Body []string
Proxies types.Proxies `json:"proxies"` Yes bool
NoProxyCheck bool
} }
func NewRequestConfig(conf *Config) *RequestConfig { func (config *RequestConfig) Print() {
return &RequestConfig{
Method: *conf.Method,
URL: conf.URL.URL,
Timeout: conf.Timeout.Duration,
DodosCount: *conf.DodosCount,
RequestCount: *conf.RequestCount,
Yes: *conf.Yes,
Params: conf.Params,
Headers: conf.Headers,
Cookies: conf.Cookies,
Body: conf.Body,
Proxies: conf.Proxies,
}
}
func (rc *RequestConfig) GetValidDodosCountForRequests() uint {
return min(rc.DodosCount, rc.RequestCount)
}
func (rc *RequestConfig) GetMaxConns(minConns uint) uint {
maxConns := max(
minConns, rc.GetValidDodosCountForRequests(),
)
return ((maxConns * 50 / 100) + maxConns)
}
func (rc *RequestConfig) Print() {
t := table.NewWriter() t := table.NewWriter()
t.SetOutputMirror(os.Stdout) t.SetOutputMirror(os.Stdout)
t.SetStyle(table.StyleLight) t.SetStyle(table.StyleLight)
t.SetColumnConfigs([]table.ColumnConfig{ t.SetColumnConfigs([]table.ColumnConfig{
{ {Number: 2, WidthMax: 50},
Number: 2,
WidthMaxEnforcer: func(col string, maxLen int) string {
lines := strings.Split(col, "\n")
for i, line := range lines {
if len(line) > maxLen {
lines[i] = line[:maxLen-3] + "..."
}
}
return strings.Join(lines, "\n")
},
WidthMax: 50},
}) })
t.AppendHeader(table.Row{"Request Configuration"}) t.AppendHeader(table.Row{"Request Configuration"})
t.AppendRow(table.Row{"URL", rc.URL.String()}) t.AppendRow(table.Row{"Method", config.Method})
t.AppendSeparator() t.AppendSeparator()
t.AppendRow(table.Row{"Method", rc.Method}) t.AppendRow(table.Row{"URL", config.URL})
t.AppendSeparator() t.AppendSeparator()
t.AppendRow(table.Row{"Timeout", rc.Timeout}) t.AppendRow(table.Row{"Timeout", fmt.Sprintf("%dms", config.Timeout/time.Millisecond)})
t.AppendSeparator() t.AppendSeparator()
t.AppendRow(table.Row{"Dodos", rc.DodosCount}) t.AppendRow(table.Row{"Dodos", config.DodosCount})
t.AppendSeparator() t.AppendSeparator()
t.AppendRow(table.Row{"Requests", rc.RequestCount}) t.AppendRow(table.Row{"Request", config.RequestCount})
t.AppendSeparator() t.AppendSeparator()
t.AppendRow(table.Row{"Params", rc.Params.String()}) t.AppendRow(table.Row{"Params", utils.MarshalJSON(config.Params, 3)})
t.AppendSeparator() t.AppendSeparator()
t.AppendRow(table.Row{"Headers", rc.Headers.String()}) t.AppendRow(table.Row{"Headers", utils.MarshalJSON(config.Headers, 3)})
t.AppendSeparator() t.AppendSeparator()
t.AppendRow(table.Row{"Cookies", rc.Cookies.String()}) t.AppendRow(table.Row{"Cookies", utils.MarshalJSON(config.Cookies, 3)})
t.AppendSeparator() t.AppendSeparator()
t.AppendRow(table.Row{"Proxy", rc.Proxies.String()}) t.AppendRow(table.Row{"Proxies Count", len(config.Proxies)})
t.AppendSeparator() t.AppendSeparator()
t.AppendRow(table.Row{"Body", rc.Body.String()}) t.AppendRow(table.Row{"Proxy Check", !config.NoProxyCheck})
t.AppendSeparator()
t.AppendRow(table.Row{"Body", utils.MarshalJSON(config.Body, 3)})
t.Render() t.Render()
} }
func (config *RequestConfig) GetValidDodosCountForRequests() uint {
return min(config.DodosCount, config.RequestCount)
}
func (config *RequestConfig) GetValidDodosCountForProxies() uint {
return min(config.DodosCount, uint(len(config.Proxies)), MaxDodosCountForProxies)
}
func (config *RequestConfig) GetMaxConns(minConns uint) uint {
maxConns := max(
minConns, uint(config.GetValidDodosCountForRequests()),
)
return ((maxConns * 50 / 100) + maxConns)
}
type Config struct { type Config struct {
Method *string `json:"method"` Method string `json:"method" validate:"http_method"` // custom validations: http_method
URL *types.RequestURL `json:"url"` URL string `json:"url" validate:"http_url,required"`
Timeout *types.Timeout `json:"timeout"` Timeout uint32 `json:"timeout" validate:"gte=1,lte=100000"`
DodosCount *uint `json:"dodos"` DodosCount uint `json:"dodos_count" validate:"gte=1"`
RequestCount *uint `json:"requests"` RequestCount uint `json:"request_count" validation_name:"request-count" validate:"gte=1"`
Yes *bool `json:"yes"` NoProxyCheck utils.Option[bool] `json:"no_proxy_check"`
Params types.Params `json:"params"`
Headers types.Headers `json:"headers"`
Cookies types.Cookies `json:"cookies"`
Body types.Body `json:"body"`
Proxies types.Proxies `json:"proxy"`
} }
func NewConfig() *Config { func (config *Config) MergeConfigs(newConfig *Config) {
return &Config{} if newConfig.Method != "" {
}
func (c *Config) Validate() []error {
var errs []error
if utils.IsNilOrZero(c.URL) {
errs = append(errs, errors.New("request URL is required"))
}
if c.URL.Scheme == "" {
c.URL.Scheme = "http"
}
if c.URL.Scheme != "http" && c.URL.Scheme != "https" {
errs = append(errs, errors.New("request URL scheme must be http or https"))
}
urlParams := types.Params{}
for key, values := range c.URL.Query() {
for _, value := range values {
urlParams = append(urlParams, types.KeyValue[string, []string]{
Key: key,
Value: []string{value},
})
}
}
c.Params = append(urlParams, c.Params...)
c.URL.RawQuery = ""
if utils.IsNilOrZero(c.Method) {
errs = append(errs, errors.New("request method is required"))
}
if utils.IsNilOrZero(c.Timeout) {
errs = append(errs, errors.New("request timeout must be greater than 0"))
}
if utils.IsNilOrZero(c.DodosCount) {
errs = append(errs, errors.New("dodos count must be greater than 0"))
}
if utils.IsNilOrZero(c.RequestCount) {
errs = append(errs, errors.New("request count must be greater than 0"))
}
for i, proxy := range c.Proxies {
if proxy.String() == "" {
errs = append(errs, fmt.Errorf("proxies[%d]: proxy cannot be empty", i))
} else if schema := proxy.Scheme; !slices.Contains(SupportedProxySchemes, schema) {
errs = append(errs,
fmt.Errorf("proxies[%d]: proxy has unsupported scheme \"%s\" (supported schemes: %s)",
i, proxy.String(), strings.Join(SupportedProxySchemes, ", "),
),
)
}
}
return errs
}
func (config *Config) MergeConfig(newConfig *Config) {
if newConfig.Method != nil {
config.Method = newConfig.Method config.Method = newConfig.Method
} }
if newConfig.URL != nil { if newConfig.URL != "" {
config.URL = newConfig.URL config.URL = newConfig.URL
} }
if newConfig.Timeout != nil { if newConfig.Timeout != 0 {
config.Timeout = newConfig.Timeout config.Timeout = newConfig.Timeout
} }
if newConfig.DodosCount != nil { if newConfig.DodosCount != 0 {
config.DodosCount = newConfig.DodosCount config.DodosCount = newConfig.DodosCount
} }
if newConfig.RequestCount != nil { if newConfig.RequestCount != 0 {
config.RequestCount = newConfig.RequestCount config.RequestCount = newConfig.RequestCount
} }
if newConfig.Yes != nil { if !newConfig.NoProxyCheck.IsNone() {
config.Yes = newConfig.Yes config.NoProxyCheck = newConfig.NoProxyCheck
} }
}
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
}
if config.NoProxyCheck.IsNone() {
config.NoProxyCheck = utils.NewOption(false)
}
}
type Proxy struct {
URL string `json:"url" validate:"required,proxy_url"`
Username string `json:"username"`
Password string `json:"password"`
}
type JSONConfig struct {
Config
Params map[string][]string `json:"params"`
Headers map[string][]string `json:"headers"`
Cookies map[string][]string `json:"cookies"`
Proxies []Proxy `json:"proxies" validate:"dive"`
Body []string `json:"body"`
}
func (config *JSONConfig) MergeConfigs(newConfig *JSONConfig) {
config.Config.MergeConfigs(&newConfig.Config)
if len(newConfig.Params) != 0 { if len(newConfig.Params) != 0 {
config.Params = newConfig.Params config.Params = newConfig.Params
} }
@ -215,20 +171,15 @@ func (config *Config) MergeConfig(newConfig *Config) {
} }
} }
func (config *Config) SetDefaults() { type CLIConfig struct {
if config.Method == nil { Config
config.Method = utils.ToPtr(DefaultMethod) Yes bool `json:"yes" validate:"omitempty"`
} ConfigFile string `validation_name:"config-file" validate:"omitempty,filepath"`
if config.Timeout == nil { }
config.Timeout = &types.Timeout{Duration: DefaultTimeout}
} func (config *CLIConfig) MergeConfigs(newConfig *CLIConfig) {
if config.DodosCount == nil { config.Config.MergeConfigs(&newConfig.Config)
config.DodosCount = utils.ToPtr(DefaultDodosCount) if newConfig.ConfigFile != "" {
} config.ConfigFile = newConfig.ConfigFile
if config.RequestCount == nil {
config.RequestCount = utils.ToPtr(DefaultRequestCount)
}
if config.Yes == nil {
config.Yes = utils.ToPtr(DefaultYes)
} }
} }

View File

@ -1,60 +0,0 @@
package config
import (
"encoding/json"
"errors"
"fmt"
"io"
"net/http"
"os"
"time"
"github.com/aykhans/dodo/types"
)
func (config *Config) ReadFile(filePath types.ConfigFile) error {
var (
data []byte
err error
)
if filePath.LocationType() == types.FileLocationTypeRemoteHTTP {
client := &http.Client{
Timeout: 10 * time.Second,
}
resp, err := client.Get(filePath.String())
if err != nil {
return fmt.Errorf("failed to fetch config file from %s", filePath)
}
defer resp.Body.Close()
data, err = io.ReadAll(io.Reader(resp.Body))
if err != nil {
return fmt.Errorf("failed to read config file from %s", filePath)
}
} else {
data, err = os.ReadFile(filePath.String())
if err != nil {
return errors.New("failed to read config file from " + filePath.String())
}
}
return parseJSONConfig(data, config)
}
func parseJSONConfig(data []byte, config *Config) error {
err := json.Unmarshal(data, &config)
if err != nil {
switch parsedErr := err.(type) {
case *json.SyntaxError:
return fmt.Errorf("JSON Config file: invalid syntax at byte offset %d", parsedErr.Offset)
case *json.UnmarshalTypeError:
return fmt.Errorf("JSON Config file: invalid type %v for field %s, expected %v", parsedErr.Value, parsedErr.Field, parsedErr.Type)
default:
return fmt.Errorf("JSON Config file: %s", err.Error())
}
}
return nil
}

117
custom_errors/errors.go Normal file
View File

@ -0,0 +1,117 @@
package customerrors
import (
"errors"
"fmt"
"github.com/go-playground/validator/v10"
)
var (
ErrInvalidJSON = errors.New("invalid JSON file")
ErrInvalidFile = errors.New("invalid file")
ErrInterrupt = errors.New("interrupted")
ErrNoInternet = errors.New("no internet connection")
ErrTimeout = errors.New("timeout")
)
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,72 @@
package customerrors
import (
"fmt"
"net"
"net/url"
"strings"
"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 shortenNamespace(namespace string) string {
return namespace[strings.Index(namespace, ".")+1:]
}
func ValidationErrorsFormater(errs validator.ValidationErrors) error {
errsStr := make(map[string]string)
for _, err := range errs {
switch err.Tag() {
case "required":
errsStr[shortenNamespace(err.Namespace())] = fmt.Sprintf("Field \"%s\" is required", err.Field())
case "gte":
errsStr[shortenNamespace(err.Namespace())] = fmt.Sprintf("Value of \"%s\" must be greater than or equal to \"%s\"", err.Field(), err.Param())
case "lte":
errsStr[shortenNamespace(err.Namespace())] = fmt.Sprintf("Value of \"%s\" must be less than or equal to \"%s\"", err.Field(), err.Param())
case "filepath":
errsStr[shortenNamespace(err.Namespace())] = fmt.Sprintf("Invalid file path for \"%s\" field: \"%s\"", err.Field(), err.Value())
case "http_url":
errsStr[shortenNamespace(err.Namespace())] =
fmt.Sprintf("Invalid url for \"%s\" field: \"%s\"", err.Field(), err.Value())
// --------------------------------------| Custom validations |--------------------------------------
case "http_method":
errsStr[shortenNamespace(err.Namespace())] = fmt.Sprintf("Invalid HTTP method for \"%s\" field: \"%s\"", err.Field(), err.Value())
case "proxy_url":
errsStr[shortenNamespace(err.Namespace())] = fmt.Sprintf("Invalid proxy url for \"%s\" field: \"%s\" (it must be http, socks5 or socks5h)", err.Field(), err.Value())
case "string_bool":
errsStr[shortenNamespace(err.Namespace())] = fmt.Sprintf("Invalid value for \"%s\" field: \"%s\"", err.Field(), err.Value())
default:
errsStr[shortenNamespace(err.Namespace())] = 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
View File

@ -1,20 +1,29 @@
module github.com/aykhans/dodo module github.com/aykhans/dodo
go 1.24.0 go 1.22.6
require ( require (
github.com/jedib0t/go-pretty/v6 v6.6.7 github.com/go-playground/validator/v10 v10.23.0
github.com/valyala/fasthttp v1.59.0 github.com/jedib0t/go-pretty/v6 v6.6.2
github.com/spf13/cobra v1.8.1
github.com/spf13/pflag v1.0.5
github.com/valyala/fasthttp v1.57.0
golang.org/x/net v0.31.0
) )
require ( require (
github.com/andybalholm/brotli v1.1.1 // indirect github.com/andybalholm/brotli v1.1.1 // indirect
github.com/gabriel-vasile/mimetype v1.4.4 // 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/klauspost/compress v1.17.11 // indirect github.com/klauspost/compress v1.17.11 // indirect
github.com/mattn/go-runewidth v0.0.16 // indirect github.com/leodido/go-urn v1.4.0 // indirect
github.com/mattn/go-runewidth v0.0.15 // indirect
github.com/rivo/uniseg v0.4.7 // indirect github.com/rivo/uniseg v0.4.7 // indirect
github.com/valyala/bytebufferpool v1.0.0 // indirect github.com/valyala/bytebufferpool v1.0.0 // indirect
golang.org/x/net v0.36.0 // indirect golang.org/x/crypto v0.29.0 // indirect
golang.org/x/sys v0.30.0 // indirect golang.org/x/sys v0.27.0 // indirect
golang.org/x/term v0.29.0 // indirect golang.org/x/term v0.26.0 // indirect
golang.org/x/text v0.22.0 // indirect golang.org/x/text v0.20.0 // indirect
) )

55
go.sum
View File

@ -1,33 +1,56 @@
github.com/andybalholm/brotli v1.1.1 h1:PR2pgnyFznKEugtsUo0xLdDop5SKXd5Qf5ysW+7XdTA= github.com/andybalholm/brotli v1.1.1 h1:PR2pgnyFznKEugtsUo0xLdDop5SKXd5Qf5ysW+7XdTA=
github.com/andybalholm/brotli v1.1.1/go.mod h1:05ib4cKhjx3OQYUY22hTVd34Bc8upXjOLL2rKwwZBoA= github.com/andybalholm/brotli v1.1.1/go.mod h1:05ib4cKhjx3OQYUY22hTVd34Bc8upXjOLL2rKwwZBoA=
github.com/cpuguy83/go-md2man/v2 v2.0.4/go.mod h1:tgQtvFlXSQOSOSIRvRPT7W67SCa46tRHOmNcaadrF8o=
github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c= 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/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
github.com/jedib0t/go-pretty/v6 v6.6.7 h1:m+LbHpm0aIAPLzLbMfn8dc3Ht8MW7lsSO4MPItz/Uuo= github.com/gabriel-vasile/mimetype v1.4.4 h1:QjV6pZ7/XZ7ryI2KuyeEDE8wnh7fHP9YnQy+R0LnH8I=
github.com/jedib0t/go-pretty/v6 v6.6.7/go.mod h1:YwC5CE4fJ1HFUDeivSV1r//AmANFHyqczZk+U6BDALU= github.com/gabriel-vasile/mimetype v1.4.4/go.mod h1:JwLei5XPtWdGiMFB5Pjle1oEeoSeEuJfJE+TtfvdB/s=
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.23.0 h1:/PwmTwZhS0dPkav3cdK9kV1FsAmrL8sThn8IHr/sO+o=
github.com/go-playground/validator/v10 v10.23.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.6.2 h1:27bLj3nRODzaiA7tPIxy9UVWHoPspFfME9XxgwiiNsM=
github.com/jedib0t/go-pretty/v6 v6.6.2/go.mod h1:zbn98qrYlh95FIhwwsbIip0LYpwSG8SUOScs+v9/t0E=
github.com/klauspost/compress v1.17.11 h1:In6xLpyWOi1+C7tXUUWv2ot1QvBjxevKAaI6IXrJmUc= github.com/klauspost/compress v1.17.11 h1:In6xLpyWOi1+C7tXUUWv2ot1QvBjxevKAaI6IXrJmUc=
github.com/klauspost/compress v1.17.11/go.mod h1:pMDklpSncoRMuLFrf1W9Ss9KT+0rH90U12bZKk7uwG0= github.com/klauspost/compress v1.17.11/go.mod h1:pMDklpSncoRMuLFrf1W9Ss9KT+0rH90U12bZKk7uwG0=
github.com/mattn/go-runewidth v0.0.16 h1:E5ScNMtiwvlvB5paMFdw9p4kSQzbXFikJ5SQO6TULQc= github.com/leodido/go-urn v1.4.0 h1:WT9HwE9SGECu3lg4d/dIA+jxlljEa1/ffXKmRjqdmIQ=
github.com/mattn/go-runewidth v0.0.16/go.mod h1:Jdepj2loyihRzMpdS35Xk/zdY8IAYHsh153qUoGf23w= 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 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM=
github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4=
github.com/rivo/uniseg v0.2.0/go.mod h1:J6wj4VEh+S6ZtnVlnTBMWIodfgj8LQOQFoIToxlJtxc= github.com/rivo/uniseg v0.2.0/go.mod h1:J6wj4VEh+S6ZtnVlnTBMWIodfgj8LQOQFoIToxlJtxc=
github.com/rivo/uniseg v0.4.7 h1:WUdvkW8uEhrYfLC4ZzdpI2ztxP1I582+49Oc5Mq64VQ= github.com/rivo/uniseg v0.4.7 h1:WUdvkW8uEhrYfLC4ZzdpI2ztxP1I582+49Oc5Mq64VQ=
github.com/rivo/uniseg v0.4.7/go.mod h1:FN3SvrM+Zdj16jyLfmOkMNblXMcoc8DfTHruCPUcx88= github.com/rivo/uniseg v0.4.7/go.mod h1:FN3SvrM+Zdj16jyLfmOkMNblXMcoc8DfTHruCPUcx88=
github.com/stretchr/testify v1.10.0 h1:Xv5erBjTwe/5IxqUQTdXv5kgmIvbHo3QQyRwhJsOfJA= github.com/russross/blackfriday/v2 v2.1.0/go.mod h1:+Rmxgy9KzJVeS9/2gXHxylqXiyQDYRxCVz55jmeOWTM=
github.com/stretchr/testify v1.10.0/go.mod h1:r2ic/lqez/lEtzL7wO/rwa5dbSLXVDPFyf8C91i36aY= github.com/spf13/cobra v1.8.1 h1:e5/vxKd/rZsfSJMUX1agtjeTDf+qv1/JdBF8gg5k9ZM=
github.com/spf13/cobra v1.8.1/go.mod h1:wHxEcudfqmLYa8iTfL+OuZPbBZkmvliBWKIezN3kD9Y=
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=
github.com/valyala/bytebufferpool v1.0.0 h1:GqA5TC/0021Y/b9FG4Oi9Mr3q7XYx6KllzawFIhcdPw= github.com/valyala/bytebufferpool v1.0.0 h1:GqA5TC/0021Y/b9FG4Oi9Mr3q7XYx6KllzawFIhcdPw=
github.com/valyala/bytebufferpool v1.0.0/go.mod h1:6bBcMArwyJ5K/AmCkWv1jt77kVWyCJ6HpOuEn7z0Csc= github.com/valyala/bytebufferpool v1.0.0/go.mod h1:6bBcMArwyJ5K/AmCkWv1jt77kVWyCJ6HpOuEn7z0Csc=
github.com/valyala/fasthttp v1.59.0 h1:Qu0qYHfXvPk1mSLNqcFtEk6DpxgA26hy6bmydotDpRI= github.com/valyala/fasthttp v1.57.0 h1:Xw8SjWGEP/+wAAgyy5XTvgrWlOD1+TxbbvNADYCm1Tg=
github.com/valyala/fasthttp v1.59.0/go.mod h1:GTxNb9Bc6r2a9D0TWNSPwDz78UxnTGBViY3xZNEqyYU= github.com/valyala/fasthttp v1.57.0/go.mod h1:h6ZBaPRlzpZ6O3H5t2gEk1Qi33+TmLvfwgLLp0t9CpE=
github.com/xyproto/randomstring v1.0.5 h1:YtlWPoRdgMu3NZtP45drfy1GKoojuR7hmRcnhZqKjWU= github.com/xyproto/randomstring v1.0.5 h1:YtlWPoRdgMu3NZtP45drfy1GKoojuR7hmRcnhZqKjWU=
github.com/xyproto/randomstring v1.0.5/go.mod h1:rgmS5DeNXLivK7YprL0pY+lTuhNQW3iGxZ18UQApw/E= github.com/xyproto/randomstring v1.0.5/go.mod h1:rgmS5DeNXLivK7YprL0pY+lTuhNQW3iGxZ18UQApw/E=
golang.org/x/net v0.36.0 h1:vWF2fRbw4qslQsQzgFqZff+BItCvGFQqKzKIzx1rmoA= golang.org/x/crypto v0.29.0 h1:L5SG1JTTXupVV3n6sUqMTeWbjAyfPwoda2DLX8J8FrQ=
golang.org/x/net v0.36.0/go.mod h1:bFmbeoIPfrw4sMHNhb4J9f6+tPziuGjq7Jk/38fxi1I= golang.org/x/crypto v0.29.0/go.mod h1:+F4F4N5hv6v38hfeYwTdx20oUvLLc+QfrE9Ax9HtgRg=
golang.org/x/sys v0.30.0 h1:QjkSwP/36a20jFYWkSue1YwXzLmsV5Gfq7Eiy72C1uc= golang.org/x/net v0.31.0 h1:68CPQngjLL0r2AlUKiSxtQFKvzRVbnzLwMUn5SzcLHo=
golang.org/x/sys v0.30.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA= golang.org/x/net v0.31.0/go.mod h1:P4fl1q7dY2hnZFxEk4pPSkDHF+QqjitcnDjUQyMM+pM=
golang.org/x/term v0.29.0 h1:L6pJp37ocefwRRtYPKSWOWzOtWSxVajvz2ldH/xi3iU= golang.org/x/sys v0.27.0 h1:wBqf8DvsY9Y/2P8gAfPDEYNuS30J4lPHJxXSb/nJZ+s=
golang.org/x/term v0.29.0/go.mod h1:6bl4lRlvVuDgSf3179VpIxBF0o10JUpXWOnI7nErv7s= golang.org/x/sys v0.27.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA=
golang.org/x/text v0.22.0 h1:bofq7m3/HAFvbF51jz3Q9wLg3jkvSPuiZu/pD1XwgtM= golang.org/x/term v0.26.0 h1:WEQa6V3Gja/BhNxg540hBip/kkaYtRg3cxg4oXSw4AU=
golang.org/x/text v0.22.0/go.mod h1:YRoo4H8PVmsu+E3Ou7cqLVH8oXWIHVoX0jqUWALQhfY= golang.org/x/term v0.26.0/go.mod h1:Si5m1o57C5nBNQo5z1iq+XDijt21BDBDp2bK0QI8e3E=
golang.org/x/text v0.20.0 h1:gK/Kv2otX8gz+wn7Rmb3vT96ZwuoxnQlY+HlJVj7Qug=
golang.org/x/text v0.20.0/go.mod h1:D4IsuqiFMhST5bX19pQ9ikHC2GsaKyk/oF+pn3ducp4=
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 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA=
gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=

117
main.go
View File

@ -2,68 +2,115 @@ package main
import ( import (
"context" "context"
"errors"
"fmt" "fmt"
"net/url"
"os" "os"
"os/signal" "os/signal"
"strings"
"syscall" "syscall"
"time"
"github.com/aykhans/dodo/config" "github.com/aykhans/dodo/config"
customerrors "github.com/aykhans/dodo/custom_errors"
"github.com/aykhans/dodo/readers"
"github.com/aykhans/dodo/requests" "github.com/aykhans/dodo/requests"
"github.com/aykhans/dodo/types"
"github.com/aykhans/dodo/utils" "github.com/aykhans/dodo/utils"
"github.com/jedib0t/go-pretty/v6/text" "github.com/aykhans/dodo/validation"
goValidator "github.com/go-playground/validator/v10"
) )
func main() { func main() {
conf := config.NewConfig() validator := validation.NewValidator()
configFile, err := conf.ReadCLI() 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.StructFiltered(
jsonConfNew,
func(ns []byte) bool {
return strings.LastIndex(string(ns), "Proxies") == -1
}); 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),
),
)
}
parsedURL, err := url.Parse(conf.URL)
if err != nil { if err != nil {
utils.PrintErrAndExit(err) utils.PrintErrAndExit(err)
} }
requestConf := &config.RequestConfig{
if configFile.String() != "" { Method: conf.Method,
tempConf := config.NewConfig() URL: parsedURL,
if err := tempConf.ReadFile(configFile); err != nil { Timeout: time.Duration(conf.Timeout) * time.Millisecond,
utils.PrintErrAndExit(err) DodosCount: conf.DodosCount,
} RequestCount: conf.RequestCount,
tempConf.MergeConfig(conf) Params: jsonConf.Params,
conf = tempConf Headers: jsonConf.Headers,
Cookies: jsonConf.Cookies,
Proxies: jsonConf.Proxies,
Body: jsonConf.Body,
Yes: cliConf.Yes,
NoProxyCheck: *conf.NoProxyCheck.ValueOrPanic(),
} }
conf.SetDefaults()
if errs := conf.Validate(); len(errs) > 0 {
utils.PrintErrAndExit(errors.Join(errs...))
}
requestConf := config.NewRequestConfig(conf)
requestConf.Print() requestConf.Print()
if !cliConf.Yes {
if !requestConf.Yes { response := readers.CLIYesOrNoReader("Do you want to continue?", true)
response := config.CLIYesOrNoReader("Do you want to continue?", false)
if !response { if !response {
utils.PrintAndExit("Exiting...\n") utils.PrintAndExit("Exiting...")
} }
fmt.Println()
} }
ctx, cancel := context.WithCancel(context.Background()) ctx, cancel := context.WithCancel(context.Background())
go listenForTermination(func() { cancel() }) sigChan := make(chan os.Signal, 1)
signal.Notify(sigChan, syscall.SIGINT, syscall.SIGTERM)
go func() {
<-sigChan
cancel()
}()
responses, err := requests.Run(ctx, requestConf) responses, err := requests.Run(ctx, requestConf)
if err != nil { if err != nil {
if err == types.ErrInterrupt { if customerrors.Is(err, customerrors.ErrInterrupt) {
fmt.Println(text.FgYellow.Sprint(err.Error())) utils.PrintlnC(utils.Colors.Yellow, err.Error())
return
} else if customerrors.Is(err, customerrors.ErrNoInternet) {
utils.PrintAndExit("No internet connection")
return return
} }
utils.PrintErrAndExit(err) panic(err)
} }
responses.Print() responses.Print()
} }
func listenForTermination(do func()) {
sigChan := make(chan os.Signal, 1)
signal.Notify(sigChan, syscall.SIGINT, syscall.SIGTERM)
<-sigChan
do()
}

100
readers/cli.go Normal file
View File

@ -0,0 +1,100 @@
package readers
import (
"fmt"
"github.com/aykhans/dodo/config"
"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{
Config: config.Config{
NoProxyCheck: utils.NewNoneOption[bool](),
},
}
dodosCount uint
requestCount uint
timeout uint32
noProxyCheck bool
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 --no-proxy-check -y`,
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().BoolVarP(&cliConfig.Yes, "yes", "y", false, "Answer yes to all questions")
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().UintVarP(&dodosCount, "dodos-count", "d", config.DefaultDodosCount, "Number of dodos(threads)")
rootCmd.Flags().UintVarP(&requestCount, "request-count", "r", config.DefaultRequestCount, "Number of total requests")
rootCmd.Flags().Uint32VarP(&timeout, "timeout", "t", config.DefaultTimeout, "Timeout for each request in milliseconds")
rootCmd.Flags().BoolVar(&noProxyCheck, "no-proxy-check", false, "Do not check for proxies")
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
case "no-proxy-check":
cliConfig.NoProxyCheck = utils.NewOption(noProxyCheck)
}
})
if returnNil {
return nil, nil
}
return cliConfig, nil
}
// CLIYesOrNoReader reads a yes or no answer from the command line.
// It prompts the user with the given message and default value,
// and returns true if the user answers "y" or "Y", and false otherwise.
// If there is an error while reading the input, it returns false.
// If the user simply presses enter without providing any input,
// it returns the default value specified by the `dft` parameter.
func CLIYesOrNoReader(message string, dft bool) bool {
var answer string
defaultMessage := "Y/n"
if !dft {
defaultMessage = "y/N"
}
fmt.Printf("%s [%s]: ", message, defaultMessage)
if _, err := fmt.Scanln(&answer); err != nil {
if err.Error() == "unexpected newline" {
return dft
}
return false
}
if answer == "" {
return dft
}
return answer == "y" || answer == "Y"
}

38
readers/json.go Normal file
View File

@ -0,0 +1,38 @@
package readers
import (
"encoding/json"
"os"
"github.com/aykhans/dodo/config"
customerrors "github.com/aykhans/dodo/custom_errors"
"github.com/aykhans/dodo/utils"
)
func JSONConfigReader(filePath string) (*config.JSONConfig, error) {
data, err := os.ReadFile(filePath)
if err != nil {
return nil, customerrors.OSErrorFormater(err)
}
jsonConf := &config.JSONConfig{
Config: config.Config{
NoProxyCheck: utils.NewNoneOption[bool](),
},
}
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
}

View File

@ -2,11 +2,14 @@ package requests
import ( import (
"context" "context"
"errors" "fmt"
"math/rand" "math/rand"
"net/url" "net/url"
"sync"
"time" "time"
"github.com/aykhans/dodo/config"
"github.com/aykhans/dodo/readers"
"github.com/aykhans/dodo/utils" "github.com/aykhans/dodo/utils"
"github.com/valyala/fasthttp" "github.com/valyala/fasthttp"
"github.com/valyala/fasthttp/fasthttpproxy" "github.com/valyala/fasthttp/fasthttpproxy"
@ -19,38 +22,80 @@ type ClientGeneratorFunc func() *fasthttp.HostClient
func getClients( func getClients(
ctx context.Context, ctx context.Context,
timeout time.Duration, timeout time.Duration,
proxies []url.URL, proxies []config.Proxy,
dodosCount uint,
maxConns uint, maxConns uint,
URL url.URL, yes bool,
noProxyCheck bool,
URL *url.URL,
) []*fasthttp.HostClient { ) []*fasthttp.HostClient {
isTLS := URL.Scheme == "https" isTLS := URL.Scheme == "https"
if proxiesLen := len(proxies); proxiesLen > 0 { if proxiesLen := len(proxies); proxiesLen > 0 {
clients := make([]*fasthttp.HostClient, 0, proxiesLen) // If noProxyCheck is true, we will return the clients without checking the proxies.
addr := URL.Host if noProxyCheck {
if isTLS && URL.Port() == "" { clients := make([]*fasthttp.HostClient, 0, proxiesLen)
addr += ":443" addr := URL.Host
} if isTLS && URL.Port() == "" {
addr += ":443"
for _, proxy := range proxies {
dialFunc, err := getDialFunc(&proxy, timeout)
if err != nil {
continue
} }
clients = append(clients, &fasthttp.HostClient{ for _, proxy := range proxies {
MaxConns: int(maxConns), dialFunc, err := getDialFunc(&proxy, timeout)
IsTLS: isTLS, if err != nil {
Addr: addr, continue
Dial: dialFunc, }
MaxIdleConnDuration: timeout,
MaxConnDuration: timeout, clients = append(clients, &fasthttp.HostClient{
WriteTimeout: timeout, MaxConns: int(maxConns),
ReadTimeout: timeout, IsTLS: isTLS,
}, Addr: addr,
Dial: dialFunc,
MaxIdleConnDuration: timeout,
MaxConnDuration: timeout,
WriteTimeout: timeout,
ReadTimeout: timeout,
},
)
}
return clients
}
// Else, we will check the proxies and return the active ones.
activeProxyClients := getActiveProxyClients(
ctx, proxies, timeout, dodosCount, maxConns, URL,
)
if ctx.Err() != nil {
return nil
}
activeProxyClientsCount := uint(len(activeProxyClients))
var yesOrNoMessage string
var yesOrNoDefault bool
if activeProxyClientsCount == 0 {
yesOrNoDefault = false
yesOrNoMessage = utils.Colored(
utils.Colors.Yellow,
"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,
),
) )
} }
return clients if !yes {
response := readers.CLIYesOrNoReader("\n"+yesOrNoMessage, yesOrNoDefault)
if !response {
utils.PrintAndExit("Exiting...")
}
}
fmt.Println()
if activeProxyClientsCount > 0 {
return activeProxyClients
}
} }
client := &fasthttp.HostClient{ client := &fasthttp.HostClient{
@ -65,19 +110,200 @@ func getClients(
return []*fasthttp.HostClient{client} return []*fasthttp.HostClient{client}
} }
// getDialFunc returns the appropriate fasthttp.DialFunc based on the provided proxy URL scheme. // getActiveProxyClients divides the proxies into slices based on the number of dodos and
// It supports SOCKS5 ('socks5' or 'socks5h') and HTTP ('http') proxy schemes. // launches goroutines to find active proxy clients for each slice.
// For HTTP proxies, the timeout parameter determines connection timeouts. // It uses a progress tracker to monitor the progress of the search.
// Returns an error if the proxy scheme is unsupported. // Once all goroutines have completed, the function waits for them to finish and
func getDialFunc(proxy *url.URL, timeout time.Duration) (fasthttp.DialFunc, error) { // returns a flattened slice of active proxy clients.
var dialer fasthttp.DialFunc func getActiveProxyClients(
ctx context.Context,
proxies []config.Proxy,
timeout time.Duration,
dodosCount uint,
maxConns uint,
URL *url.URL,
) []*fasthttp.HostClient {
activeProxyClientsArray := make([][]*fasthttp.HostClient, dodosCount)
proxiesCount := len(proxies)
dodosCountInt := int(dodosCount)
if proxy.Scheme == "socks5" || proxy.Scheme == "socks5h" { var (
dialer = fasthttpproxy.FasthttpSocksDialerDualStack(proxy.String()) wg sync.WaitGroup
} else if proxy.Scheme == "http" { streamWG sync.WaitGroup
dialer = fasthttpproxy.FasthttpHTTPDialerDualStackTimeout(proxy.String(), timeout) )
wg.Add(dodosCountInt)
streamWG.Add(1)
var proxiesSlice []config.Proxy
increase := make(chan int64, proxiesCount)
streamCtx, streamCtxCancel := context.WithCancel(context.Background())
go streamProgress(streamCtx, &streamWG, int64(proxiesCount), "Searching for active proxies🌐", increase)
for i := range dodosCountInt {
if i+1 == dodosCountInt {
proxiesSlice = proxies[i*proxiesCount/dodosCountInt:]
} else {
proxiesSlice = proxies[i*proxiesCount/dodosCountInt : (i+1)*proxiesCount/dodosCountInt]
}
go findActiveProxyClients(
ctx,
proxiesSlice,
timeout,
&activeProxyClientsArray[i],
increase,
maxConns,
URL,
&wg,
)
}
wg.Wait()
streamCtxCancel()
streamWG.Wait()
return utils.Flatten(activeProxyClientsArray)
}
// findActiveProxyClients checks a list of proxies to determine which ones are active
// and appends the active ones to the provided activeProxyClients slice.
//
// Parameters:
// - ctx: The context to control cancellation and timeout.
// - proxies: A slice of Proxy configurations to be checked.
// - timeout: The duration to wait for each proxy check before timing out.
// - activeProxyClients: A pointer to a slice where active proxy clients will be appended.
// - increase: A channel to signal the increase of checked proxies count.
// - URL: The URL to be used for checking the proxies.
// - wg: A WaitGroup to signal when the function is done.
//
// The function sends a GET request to each proxy using the provided URL. If the proxy
// responds with a status code of 200, it is considered active and added to the activeProxyClients slice.
// The function respects the context's cancellation and timeout settings.
func findActiveProxyClients(
ctx context.Context,
proxies []config.Proxy,
timeout time.Duration,
activeProxyClients *[]*fasthttp.HostClient,
increase chan<- int64,
maxConns uint,
URL *url.URL,
wg *sync.WaitGroup,
) {
defer wg.Done()
request := fasthttp.AcquireRequest()
defer fasthttp.ReleaseRequest(request)
request.SetRequestURI(config.ProxyCheckURL)
request.Header.SetMethod("GET")
for _, proxy := range proxies {
if ctx.Err() != nil {
return
}
func() {
defer func() { increase <- 1 }()
response := fasthttp.AcquireResponse()
defer fasthttp.ReleaseResponse(response)
dialFunc, err := getDialFunc(&proxy, timeout)
if err != nil {
return
}
client := &fasthttp.Client{
Dial: dialFunc,
}
defer client.CloseIdleConnections()
ch := make(chan error)
go func() {
err := client.DoTimeout(request, response, timeout)
ch <- err
}()
select {
case err := <-ch:
if err != nil {
return
}
break
case <-time.After(timeout):
return
case <-ctx.Done():
return
}
isTLS := URL.Scheme == "https"
addr := URL.Host
if isTLS && URL.Port() == "" {
addr += ":443"
}
if response.StatusCode() == 200 {
*activeProxyClients = append(
*activeProxyClients,
&fasthttp.HostClient{
MaxConns: int(maxConns),
IsTLS: isTLS,
Addr: addr,
Dial: dialFunc,
MaxIdleConnDuration: timeout,
MaxConnDuration: timeout,
WriteTimeout: timeout,
ReadTimeout: timeout,
},
)
}
}()
}
}
// getDialFunc returns a fasthttp.DialFunc based on the provided proxy configuration.
// It takes a pointer to a config.Proxy struct as input and returns a fasthttp.DialFunc and an error.
// The function parses the proxy URL, determines the scheme (socks5, socks5h, http, or https),
// and creates a dialer accordingly. If the proxy URL is invalid or the scheme is not supported,
// it returns an error.
func getDialFunc(proxy *config.Proxy, timeout time.Duration) (fasthttp.DialFunc, error) {
parsedProxyURL, err := url.Parse(proxy.URL)
if err != nil {
return nil, err
}
var dialer fasthttp.DialFunc
if parsedProxyURL.Scheme == "socks5" || parsedProxyURL.Scheme == "socks5h" {
if proxy.Username != "" {
dialer = fasthttpproxy.FasthttpSocksDialer(
fmt.Sprintf(
"%s://%s:%s@%s",
parsedProxyURL.Scheme,
proxy.Username,
proxy.Password,
parsedProxyURL.Host,
),
)
} else {
dialer = fasthttpproxy.FasthttpSocksDialer(
fmt.Sprintf(
"%s://%s",
parsedProxyURL.Scheme,
parsedProxyURL.Host,
),
)
}
} else if parsedProxyURL.Scheme == "http" {
if proxy.Username != "" {
dialer = fasthttpproxy.FasthttpHTTPDialerTimeout(
fmt.Sprintf(
"%s:%s@%s",
proxy.Username, proxy.Password, parsedProxyURL.Host,
),
timeout,
)
} else {
dialer = fasthttpproxy.FasthttpHTTPDialerTimeout(
parsedProxyURL.Host,
timeout,
)
}
} else { } else {
return nil, errors.New("unsupported proxy scheme") return nil, err
} }
return dialer, nil return dialer, nil
} }

View File

@ -7,6 +7,7 @@ import (
"time" "time"
"github.com/jedib0t/go-pretty/v6/progress" "github.com/jedib0t/go-pretty/v6/progress"
"github.com/valyala/fasthttp"
) )
// streamProgress streams the progress of a task to the console using a progress bar. // streamProgress streams the progress of a task to the console using a progress bar.
@ -36,11 +37,9 @@ func streamProgress(
for { for {
select { select {
case <-ctx.Done(): case <-ctx.Done():
if ctx.Err() != context.Canceled {
dodosTracker.MarkAsErrored()
}
fmt.Printf("\r") fmt.Printf("\r")
time.Sleep(time.Millisecond * 500) dodosTracker.MarkAsErrored()
time.Sleep(time.Millisecond * 300)
pw.Stop() pw.Stop()
return return
@ -49,3 +48,28 @@ func streamProgress(
} }
} }
} }
// checkConnection checks the internet connection by making requests to different websites.
// It returns true if the connection is successful, otherwise false.
func checkConnection(ctx context.Context) bool {
ch := make(chan bool)
go func() {
_, _, err := fasthttp.Get(nil, "https://www.google.com")
if err != nil {
_, _, err = fasthttp.Get(nil, "https://www.bing.com")
if err != nil {
_, _, err = fasthttp.Get(nil, "https://www.yahoo.com")
ch <- err == nil
}
ch <- true
}
ch <- true
}()
select {
case <-ctx.Done():
return false
case res := <-ch:
return res
}
}

View File

@ -7,7 +7,7 @@ import (
"time" "time"
"github.com/aykhans/dodo/config" "github.com/aykhans/dodo/config"
"github.com/aykhans/dodo/types" customerrors "github.com/aykhans/dodo/custom_errors"
"github.com/aykhans/dodo/utils" "github.com/aykhans/dodo/utils"
"github.com/valyala/fasthttp" "github.com/valyala/fasthttp"
) )
@ -26,6 +26,7 @@ type Request struct {
func (r *Request) Send(ctx context.Context, timeout time.Duration) (*fasthttp.Response, error) { func (r *Request) Send(ctx context.Context, timeout time.Duration) (*fasthttp.Response, error) {
client := r.getClient() client := r.getClient()
request := r.getRequest() request := r.getRequest()
defer client.CloseIdleConnections()
defer fasthttp.ReleaseRequest(request) defer fasthttp.ReleaseRequest(request)
response := fasthttp.AcquireResponse() response := fasthttp.AcquireResponse()
@ -43,9 +44,9 @@ func (r *Request) Send(ctx context.Context, timeout time.Duration) (*fasthttp.Re
return response, nil return response, nil
case <-time.After(timeout): case <-time.After(timeout):
fasthttp.ReleaseResponse(response) fasthttp.ReleaseResponse(response)
return nil, types.ErrTimeout return nil, customerrors.ErrTimeout
case <-ctx.Done(): case <-ctx.Done():
return nil, types.ErrInterrupt return nil, customerrors.ErrInterrupt
} }
} }
@ -74,9 +75,9 @@ func newRequest(
getRequest := getRequestGeneratorFunc( getRequest := getRequestGeneratorFunc(
requestConfig.URL, requestConfig.URL,
requestConfig.Params,
requestConfig.Headers, requestConfig.Headers,
requestConfig.Cookies, requestConfig.Cookies,
requestConfig.Params,
requestConfig.Method, requestConfig.Method,
requestConfig.Body, requestConfig.Body,
localRand, localRand,
@ -90,36 +91,37 @@ func newRequest(
return requests return requests
} }
// getRequestGeneratorFunc returns a RequestGeneratorFunc which generates HTTP requests with the specified parameters. // getRequestGeneratorFunc returns a RequestGeneratorFunc which generates HTTP requests
// The function uses a local random number generator to select bodies, headers, cookies, and parameters if multiple options are provided. // with the specified parameters.
// The function uses a local random number generator to select bodies, headers, cookies, and parameters
// if multiple options are provided.
func getRequestGeneratorFunc( func getRequestGeneratorFunc(
URL url.URL, URL *url.URL,
params types.Params, Headers map[string][]string,
headers types.Headers, Cookies map[string][]string,
cookies types.Cookies, Params map[string][]string,
method string, Method string,
bodies []string, Bodies []string,
localRand *rand.Rand, localRand *rand.Rand,
) RequestGeneratorFunc { ) RequestGeneratorFunc {
bodiesLen := len(bodies) bodiesLen := len(Bodies)
getBody := func() string { return "" } getBody := func() string { return "" }
if bodiesLen == 1 { if bodiesLen == 1 {
getBody = func() string { return bodies[0] } getBody = func() string { return Bodies[0] }
} else if bodiesLen > 1 { } else if bodiesLen > 1 {
getBody = utils.RandomValueCycle(bodies, localRand) getBody = utils.RandomValueCycle(Bodies, localRand)
} }
getHeaders := getKeyValueSetFunc(Headers, localRand)
getParams := getKeyValueGeneratorFunc(params, localRand) getCookies := getKeyValueSetFunc(Cookies, localRand)
getHeaders := getKeyValueGeneratorFunc(headers, localRand) getParams := getKeyValueSetFunc(Params, localRand)
getCookies := getKeyValueGeneratorFunc(cookies, localRand)
return func() *fasthttp.Request { return func() *fasthttp.Request {
return newFasthttpRequest( return newFasthttpRequest(
URL, URL,
getParams(),
getHeaders(), getHeaders(),
getCookies(), getCookies(),
method, getParams(),
Method,
getBody(), getBody(),
) )
} }
@ -128,12 +130,12 @@ func getRequestGeneratorFunc(
// newFasthttpRequest creates a new fasthttp.Request object with the provided parameters. // newFasthttpRequest creates a new fasthttp.Request object with the provided parameters.
// It sets the request URI, host header, headers, cookies, params, method, and body. // It sets the request URI, host header, headers, cookies, params, method, and body.
func newFasthttpRequest( func newFasthttpRequest(
URL url.URL, URL *url.URL,
params []types.KeyValue[string, string], Headers map[string]string,
headers []types.KeyValue[string, string], Cookies map[string]string,
cookies []types.KeyValue[string, string], Params map[string]string,
method string, Method string,
body string, Body string,
) *fasthttp.Request { ) *fasthttp.Request {
request := fasthttp.AcquireRequest() request := fasthttp.AcquireRequest()
request.SetRequestURI(URL.Path) request.SetRequestURI(URL.Path)
@ -141,12 +143,12 @@ func newFasthttpRequest(
// Set the host of the request to the host header // Set the host of the request to the host header
// If the host header is not set, the request will fail // If the host header is not set, the request will fail
// If there is host header in the headers, it will be overwritten // If there is host header in the headers, it will be overwritten
request.Header.SetHost(URL.Host) request.Header.Set("Host", URL.Host)
setRequestParams(request, params) setRequestHeaders(request, Headers)
setRequestHeaders(request, headers) setRequestCookies(request, Cookies)
setRequestCookies(request, cookies) setRequestParams(request, Params)
setRequestMethod(request, method) setRequestMethod(request, Method)
setRequestBody(request, body) setRequestBody(request, Body)
if URL.Scheme == "https" { if URL.Scheme == "https" {
request.URI().SetScheme("https") request.URI().SetScheme("https")
} }
@ -154,28 +156,28 @@ func newFasthttpRequest(
return request return request
} }
// setRequestParams adds the query parameters of the given request based on the provided key-value pairs. // setRequestHeaders sets the headers of the given request with the provided key-value pairs.
func setRequestParams(req *fasthttp.Request, params []types.KeyValue[string, string]) { func setRequestHeaders(req *fasthttp.Request, headers map[string]string) {
for _, param := range params { req.Header.Set("User-Agent", config.DefaultUserAgent)
req.URI().QueryArgs().Add(param.Key, param.Value) for key, value := range headers {
req.Header.Set(key, value)
} }
} }
// setRequestHeaders adds the headers of the given request with the provided key-value pairs. // setRequestCookies sets the cookies in the given request.
func setRequestHeaders(req *fasthttp.Request, headers []types.KeyValue[string, string]) { func setRequestCookies(req *fasthttp.Request, cookies map[string]string) {
for _, header := range headers { for key, value := range cookies {
req.Header.Add(header.Key, header.Value) req.Header.SetCookie(key, value)
}
if req.Header.UserAgent() == nil {
req.Header.SetUserAgent(config.DefaultUserAgent)
} }
} }
// setRequestCookies adds the cookies of the given request with the provided key-value pairs. // setRequestParams sets the query parameters of the given request based on the provided map of key-value pairs.
func setRequestCookies(req *fasthttp.Request, cookies []types.KeyValue[string, string]) { func setRequestParams(req *fasthttp.Request, params map[string]string) {
for _, cookie := range cookies { urlParams := url.Values{}
req.Header.Add("Cookie", cookie.Key+"="+cookie.Value) for key, value := range params {
urlParams.Add(key, value)
} }
req.URI().SetQueryString(urlParams.Encode())
} }
// setRequestMethod sets the HTTP request method for the given request. // setRequestMethod sets the HTTP request method for the given request.
@ -189,62 +191,59 @@ func setRequestBody(req *fasthttp.Request, body string) {
req.SetBody([]byte(body)) req.SetBody([]byte(body))
} }
// getKeyValueGeneratorFunc creates a function that generates key-value pairs for HTTP requests. // getKeyValueSetFunc generates a function that returns a map of key-value pairs based on the provided key-value set.
// It takes a slice of key-value pairs where each key maps to a slice of possible values, // The generated function will either return fixed values or random values depending on the input.
// and a random number generator.
// //
// If any key has multiple possible values, the function will randomly select one value for each // Returns:
// call (using the provided random number generator). If all keys have at most one value, the // - A function that returns a map of key-value pairs. If the input map contains multiple values for a key,
// function will always return the same set of key-value pairs for efficiency. // the returned function will generate random values for that key. If the input map contains a single value
func getKeyValueGeneratorFunc[ // for a key, the returned function will always return that value. If the input map is empty for a key,
T []types.KeyValue[string, string], // the returned function will generate an empty string for that key.
]( func getKeyValueSetFunc[
keyValueSlice []types.KeyValue[string, []string], KeyValueSet map[string][]string,
localRand *rand.Rand, KeyValue map[string]string,
) func() T { ](keyValueSet KeyValueSet, localRand *rand.Rand) func() KeyValue {
getKeyValueSlice := []map[string]func() string{} getKeyValueSlice := []map[string]func() string{}
isRandom := false isRandom := false
for key, values := range keyValueSet {
valuesLen := len(values)
for _, kv := range keyValueSlice { // if values is empty, return a function that generates empty string
valuesLen := len(kv.Value) // if values has only one element, return a function that generates that element
// if values has more than one element, return a function that generates a random element
getValueFunc := func() string { return "" } getKeyValue := func() string { return "" }
if valuesLen == 1 { if valuesLen == 1 {
getValueFunc = func() string { return kv.Value[0] } getKeyValue = func() string { return values[0] }
} else if valuesLen > 1 { } else if valuesLen > 1 {
getValueFunc = utils.RandomValueCycle(kv.Value, localRand) getKeyValue = utils.RandomValueCycle(values, localRand)
isRandom = true isRandom = true
} }
getKeyValueSlice = append( getKeyValueSlice = append(
getKeyValueSlice, getKeyValueSlice,
map[string]func() string{kv.Key: getValueFunc}, map[string]func() string{key: getKeyValue},
) )
} }
// if isRandom is true, return a function that generates random values,
// otherwise return a function that generates fixed values to avoid unnecessary random number generation
if isRandom { if isRandom {
return func() T { return func() KeyValue {
keyValues := make(T, len(getKeyValueSlice)) keyValues := make(KeyValue, len(getKeyValueSlice))
for i, keyValue := range getKeyValueSlice { for _, keyValue := range getKeyValueSlice {
for key, value := range keyValue { for key, value := range keyValue {
keyValues[i] = types.KeyValue[string, string]{ keyValues[key] = value()
Key: key,
Value: value(),
}
} }
} }
return keyValues return keyValues
} }
} else { } else {
keyValues := make(T, len(getKeyValueSlice)) keyValues := make(KeyValue, len(getKeyValueSlice))
for i, keyValue := range getKeyValueSlice { for _, keyValue := range getKeyValueSlice {
for key, value := range keyValue { for key, value := range keyValue {
keyValues[i] = types.KeyValue[string, string]{ keyValues[key] = value()
Key: key,
Value: value(),
}
} }
} }
return func() T { return keyValues } return func() KeyValue { return keyValues }
} }
} }

View File

@ -1,108 +1,85 @@
package requests package requests
import ( import (
"fmt"
"os" "os"
"time" "time"
"github.com/aykhans/dodo/types"
"github.com/aykhans/dodo/utils" "github.com/aykhans/dodo/utils"
"github.com/jedib0t/go-pretty/v6/table" "github.com/jedib0t/go-pretty/v6/table"
) )
type Response struct { type Response struct {
Response string StatusCode int
Time time.Duration Error error
Time time.Duration
} }
type Responses []*Response type Responses []*Response
// Print prints the responses in a tabular format, including information such as // Print prints the responses in a tabular format, including information such as
// response count, minimum time, maximum time, average time, and latency percentiles. // response count, minimum time, maximum time, and average time.
func (responses Responses) Print() { func (respones Responses) Print() {
total := struct { var (
Count int totalMinDuration time.Duration = respones[0].Time
Min time.Duration totalMaxDuration time.Duration = respones[0].Time
Max time.Duration totalDuration time.Duration
Sum time.Duration totalCount int = len(respones)
P90 time.Duration )
P95 time.Duration mergedResponses := make(map[string][]time.Duration)
P99 time.Duration
}{
Count: len(responses),
Min: responses[0].Time,
Max: responses[0].Time,
}
mergedResponses := make(map[string]types.Durations)
var allDurations types.Durations
for _, response := range responses { for _, response := range respones {
if response.Time < total.Min { if response.Time < totalMinDuration {
total.Min = response.Time totalMinDuration = response.Time
} }
if response.Time > total.Max { if response.Time > totalMaxDuration {
total.Max = response.Time totalMaxDuration = response.Time
} }
total.Sum += response.Time totalDuration += response.Time
mergedResponses[response.Response] = append( if response.Error != nil {
mergedResponses[response.Response], mergedResponses[response.Error.Error()] = append(
response.Time, mergedResponses[response.Error.Error()],
) response.Time,
allDurations = append(allDurations, response.Time) )
} else {
mergedResponses[fmt.Sprintf("%d", response.StatusCode)] = append(
mergedResponses[fmt.Sprintf("%d", response.StatusCode)],
response.Time,
)
}
} }
allDurations.Sort()
allDurationsLenAsFloat := float64(len(allDurations) - 1)
total.P90 = allDurations[int(0.90*allDurationsLenAsFloat)]
total.P95 = allDurations[int(0.95*allDurationsLenAsFloat)]
total.P99 = allDurations[int(0.99*allDurationsLenAsFloat)]
t := table.NewWriter() t := table.NewWriter()
t.SetOutputMirror(os.Stdout) t.SetOutputMirror(os.Stdout)
t.SetStyle(table.StyleLight) t.SetStyle(table.StyleLight)
t.SetColumnConfigs([]table.ColumnConfig{ t.SetColumnConfigs([]table.ColumnConfig{
{Number: 1, WidthMax: 40}, {Number: 1, WidthMax: 80},
}) })
t.AppendHeader(table.Row{ t.AppendHeader(table.Row{
"Response", "Response",
"Count", "Count",
"Min", "Min Time",
"Max", "Max Time",
"Average", "Average Time",
"P90",
"P95",
"P99",
}) })
var roundPrecision int64 = 4
for key, durations := range mergedResponses { for key, durations := range mergedResponses {
durations.Sort()
durationsLen := len(durations)
durationsLenAsFloat := float64(durationsLen - 1)
t.AppendRow(table.Row{ t.AppendRow(table.Row{
key, key,
durationsLen, len(durations),
utils.DurationRoundBy(*durations.First(), roundPrecision), utils.MinDuration(durations...),
utils.DurationRoundBy(*durations.Last(), roundPrecision), utils.MaxDuration(durations...),
utils.DurationRoundBy(durations.Avg(), roundPrecision), utils.AvgDuration(durations...),
utils.DurationRoundBy(durations[int(0.90*durationsLenAsFloat)], roundPrecision),
utils.DurationRoundBy(durations[int(0.95*durationsLenAsFloat)], roundPrecision),
utils.DurationRoundBy(durations[int(0.99*durationsLenAsFloat)], roundPrecision),
}) })
t.AppendSeparator() t.AppendSeparator()
} }
t.AppendRow(table.Row{
if len(mergedResponses) > 1 { "Total",
t.AppendRow(table.Row{ totalCount,
"Total", totalMinDuration,
total.Count, totalMaxDuration,
utils.DurationRoundBy(total.Min, roundPrecision), totalDuration / time.Duration(totalCount),
utils.DurationRoundBy(total.Max, roundPrecision), })
utils.DurationRoundBy(total.Sum/time.Duration(total.Count), roundPrecision), // Average
utils.DurationRoundBy(total.P90, roundPrecision),
utils.DurationRoundBy(total.P95, roundPrecision),
utils.DurationRoundBy(total.P99, roundPrecision),
})
}
t.Render() t.Render()
} }

View File

@ -2,38 +2,52 @@ package requests
import ( import (
"context" "context"
"strconv"
"sync" "sync"
"time" "time"
"github.com/aykhans/dodo/config" "github.com/aykhans/dodo/config"
"github.com/aykhans/dodo/types" customerrors "github.com/aykhans/dodo/custom_errors"
"github.com/aykhans/dodo/utils" "github.com/aykhans/dodo/utils"
"github.com/valyala/fasthttp" "github.com/valyala/fasthttp"
) )
// Run executes the main logic for processing requests based on the provided configuration. // Run executes the main logic for processing requests based on the provided configuration.
// It initializes clients based on the request configuration and releases the dodos. // It first checks for an internet connection with a timeout context. If no connection is found,
// If the context is canceled and no responses are collected, it returns an interrupt error. // it returns an error. Then, it initializes clients based on the request configuration and
// releases the dodos. If the context is canceled and no responses are collected, it returns an interrupt error.
// //
// Parameters: // Parameters:
// - ctx: The context for managing request lifecycle and cancellation. // - ctx: The context for managing request lifecycle and cancellation.
// - requestConfig: The configuration for the request, including timeout, proxies, and other settings. // - requestConfig: The configuration for the request, including timeout, proxies, and other settings.
//
// Returns:
// - Responses: A collection of responses from the executed requests.
// - error: An error if the operation fails, such as no internet connection or an interrupt.
func Run(ctx context.Context, requestConfig *config.RequestConfig) (Responses, error) { func Run(ctx context.Context, requestConfig *config.RequestConfig) (Responses, error) {
checkConnectionCtx, checkConnectionCtxCancel := context.WithTimeout(ctx, 8*time.Second)
if !checkConnection(checkConnectionCtx) {
checkConnectionCtxCancel()
return nil, customerrors.ErrNoInternet
}
checkConnectionCtxCancel()
clients := getClients( clients := getClients(
ctx, ctx,
requestConfig.Timeout, requestConfig.Timeout,
requestConfig.Proxies, requestConfig.Proxies,
requestConfig.GetValidDodosCountForProxies(),
requestConfig.GetMaxConns(fasthttp.DefaultMaxConnsPerHost), requestConfig.GetMaxConns(fasthttp.DefaultMaxConnsPerHost),
requestConfig.Yes,
requestConfig.NoProxyCheck,
requestConfig.URL, requestConfig.URL,
) )
if clients == nil { if clients == nil {
return nil, types.ErrInterrupt return nil, customerrors.ErrInterrupt
} }
responses := releaseDodos(ctx, requestConfig, clients) responses := releaseDodos(ctx, requestConfig, clients)
if ctx.Err() != nil && len(responses) == 0 { if ctx.Err() != nil && len(responses) == 0 {
return nil, types.ErrInterrupt return nil, customerrors.ErrInterrupt
} }
return responses, nil return responses, nil
@ -60,22 +74,23 @@ func releaseDodos(
requestCountPerDodo uint requestCountPerDodo uint
dodosCount uint = requestConfig.GetValidDodosCountForRequests() dodosCount uint = requestConfig.GetValidDodosCountForRequests()
dodosCountInt int = int(dodosCount) dodosCountInt int = int(dodosCount)
requestCount uint = uint(requestConfig.RequestCount)
responses = make([][]*Response, dodosCount) responses = make([][]*Response, dodosCount)
increase = make(chan int64, requestConfig.RequestCount) increase = make(chan int64, requestCount)
) )
wg.Add(dodosCountInt) wg.Add(dodosCountInt)
streamWG.Add(1) streamWG.Add(1)
streamCtx, streamCtxCancel := context.WithCancel(context.Background()) streamCtx, streamCtxCancel := context.WithCancel(context.Background())
go streamProgress(streamCtx, &streamWG, int64(requestConfig.RequestCount), "Dodos Working🔥", increase) go streamProgress(streamCtx, &streamWG, int64(requestCount), "Dodos Working🔥", increase)
for i := range dodosCount { for i := range dodosCount {
if i+1 == dodosCount { if i+1 == dodosCount {
requestCountPerDodo = requestConfig.RequestCount - (i * requestConfig.RequestCount / dodosCount) requestCountPerDodo = requestCount - (i * requestCount / dodosCount)
} else { } else {
requestCountPerDodo = ((i + 1) * requestConfig.RequestCount / dodosCount) - requestCountPerDodo = ((i + 1) * requestCount / dodosCount) -
(i * requestConfig.RequestCount / dodosCount) (i * requestCount / dodosCount)
} }
go sendRequest( go sendRequest(
@ -123,20 +138,22 @@ func sendRequest(
} }
if err != nil { if err != nil {
if err == types.ErrInterrupt { if err == customerrors.ErrInterrupt {
return return
} }
*responseData = append(*responseData, &Response{ *responseData = append(*responseData, &Response{
Response: err.Error(), StatusCode: 0,
Time: completedTime, Error: err,
Time: completedTime,
}) })
increase <- 1 increase <- 1
return return
} }
*responseData = append(*responseData, &Response{ *responseData = append(*responseData, &Response{
Response: strconv.Itoa(response.StatusCode()), StatusCode: response.StatusCode(),
Time: completedTime, Error: nil,
Time: completedTime,
}) })
increase <- 1 increase <- 1
}() }()

View File

@ -1,72 +0,0 @@
package types
import (
"bytes"
"encoding/json"
"fmt"
"github.com/jedib0t/go-pretty/v6/text"
)
type Body []string
func (body Body) String() string {
var buffer bytes.Buffer
if len(body) == 0 {
return string(buffer.Bytes())
}
if len(body) == 1 {
buffer.WriteString(body[0])
return string(buffer.Bytes())
}
buffer.WriteString(text.FgBlue.Sprint("Random") + "[\n")
indent := " "
displayLimit := 5
for i, item := range body[:min(len(body), displayLimit)] {
if i > 0 {
buffer.WriteString(",\n")
}
buffer.WriteString(indent + item)
}
// Add remaining count if there are more items
if remainingValues := len(body) - displayLimit; remainingValues > 0 {
buffer.WriteString(",\n" + indent + text.FgGreen.Sprintf("+%d bodies", remainingValues))
}
buffer.WriteString("\n]")
return string(buffer.Bytes())
}
func (body *Body) UnmarshalJSON(b []byte) error {
var data any
if err := json.Unmarshal(b, &data); err != nil {
return err
}
switch v := data.(type) {
case string:
*body = []string{v}
case []any:
var slice []string
for _, item := range v {
slice = append(slice, fmt.Sprintf("%v", item))
}
*body = slice
default:
return fmt.Errorf("invalid type for Body: %T (should be string or []string)", v)
}
return nil
}
func (body *Body) Set(value string) error {
*body = append(*body, value)
return nil
}

View File

@ -1,23 +0,0 @@
package types
import "strings"
type FileLocationType int
const (
FileLocationTypeLocal FileLocationType = iota
FileLocationTypeRemoteHTTP
)
type ConfigFile string
func (config ConfigFile) String() string {
return string(config)
}
func (config ConfigFile) LocationType() FileLocationType {
if strings.HasPrefix(string(config), "http://") || strings.HasPrefix(string(config), "https://") {
return FileLocationTypeRemoteHTTP
}
return FileLocationTypeLocal
}

View File

@ -1,114 +0,0 @@
package types
import (
"bytes"
"encoding/json"
"fmt"
"strings"
"github.com/jedib0t/go-pretty/v6/text"
)
type Cookies []KeyValue[string, []string]
func (cookies Cookies) String() string {
var buffer bytes.Buffer
if len(cookies) == 0 {
return string(buffer.Bytes())
}
indent := " "
displayLimit := 3
for i, item := range cookies[:min(len(cookies), displayLimit)] {
if i > 0 {
buffer.WriteString(",\n")
}
if len(item.Value) == 1 {
buffer.WriteString(item.Key + ": " + item.Value[0])
continue
}
buffer.WriteString(item.Key + ": " + text.FgBlue.Sprint("Random") + "[\n")
for ii, v := range item.Value[:min(len(item.Value), displayLimit)] {
if ii == len(item.Value)-1 {
buffer.WriteString(indent + v + "\n")
} else {
buffer.WriteString(indent + v + ",\n")
}
}
// Add remaining values count if needed
if remainingValues := len(item.Value) - displayLimit; remainingValues > 0 {
buffer.WriteString(indent + text.FgGreen.Sprintf("+%d values", remainingValues) + "\n")
}
buffer.WriteString("]")
}
// Add remaining key-value pairs count if needed
if remainingPairs := len(cookies) - displayLimit; remainingPairs > 0 {
buffer.WriteString(",\n" + text.FgGreen.Sprintf("+%d cookies", remainingPairs))
}
return string(buffer.Bytes())
}
func (cookies *Cookies) UnmarshalJSON(b []byte) error {
var data []map[string]any
if err := json.Unmarshal(b, &data); err != nil {
return err
}
for _, item := range data {
for key, value := range item {
switch parsedValue := value.(type) {
case string:
*cookies = append(*cookies, KeyValue[string, []string]{Key: key, Value: []string{parsedValue}})
case []any:
parsedStr := make([]string, len(parsedValue))
for i, item := range parsedValue {
parsedStr[i] = fmt.Sprintf("%v", item)
}
*cookies = append(*cookies, KeyValue[string, []string]{Key: key, Value: parsedStr})
default:
return fmt.Errorf("unsupported type for cookies expected string or []string, got %T", parsedValue)
}
}
}
return nil
}
func (cookies *Cookies) Set(value string) error {
parts := strings.SplitN(value, "=", 2)
switch len(parts) {
case 0:
cookies.AppendByKey("", "")
case 1:
cookies.AppendByKey(parts[0], "")
case 2:
cookies.AppendByKey(parts[0], parts[1])
}
return nil
}
func (cookies *Cookies) AppendByKey(key, value string) {
if item := cookies.GetValue(key); item != nil {
*item = append(*item, value)
} else {
*cookies = append(*cookies, KeyValue[string, []string]{Key: key, Value: []string{value}})
}
}
func (cookies Cookies) GetValue(key string) *[]string {
for i := range cookies {
if cookies[i].Key == key {
return &cookies[i].Value
}
}
return nil
}

View File

@ -1,36 +0,0 @@
package types
import (
"encoding/json"
"errors"
"time"
)
type Timeout struct {
time.Duration
}
func (timeout *Timeout) UnmarshalJSON(b []byte) error {
var v any
if err := json.Unmarshal(b, &v); err != nil {
return err
}
switch value := v.(type) {
case float64:
timeout.Duration = time.Duration(value)
return nil
case string:
var err error
timeout.Duration, err = time.ParseDuration(value)
if err != nil {
return errors.New("Timeout is invalid (e.g. 400ms, 1s, 5m, 1h)")
}
return nil
default:
return errors.New("Timeout is invalid (e.g. 400ms, 1s, 5m, 1h)")
}
}
func (timeout Timeout) MarshalJSON() ([]byte, error) {
return json.Marshal(timeout.Duration.String())
}

View File

@ -1,41 +0,0 @@
package types
import (
"sort"
"time"
)
type Durations []time.Duration
func (d Durations) Sort(ascending ...bool) {
// If ascending is provided and is false, sort in descending order
if len(ascending) > 0 && ascending[0] == false {
sort.Slice(d, func(i, j int) bool {
return d[i] > d[j]
})
} else { // Otherwise, sort in ascending order
sort.Slice(d, func(i, j int) bool {
return d[i] < d[j]
})
}
}
func (d Durations) First() *time.Duration {
return &d[0]
}
func (d Durations) Last() *time.Duration {
return &d[len(d)-1]
}
func (d Durations) Sum() time.Duration {
sum := time.Duration(0)
for _, duration := range d {
sum += duration
}
return sum
}
func (d Durations) Avg() time.Duration {
return d.Sum() / time.Duration(len(d))
}

View File

@ -1,10 +0,0 @@
package types
import (
"errors"
)
var (
ErrInterrupt = errors.New("interrupted")
ErrTimeout = errors.New("timeout")
)

View File

@ -1,114 +0,0 @@
package types
import (
"bytes"
"encoding/json"
"fmt"
"strings"
"github.com/jedib0t/go-pretty/v6/text"
)
type Headers []KeyValue[string, []string]
func (headers Headers) String() string {
var buffer bytes.Buffer
if len(headers) == 0 {
return string(buffer.Bytes())
}
indent := " "
displayLimit := 3
for i, item := range headers[:min(len(headers), displayLimit)] {
if i > 0 {
buffer.WriteString(",\n")
}
if len(item.Value) == 1 {
buffer.WriteString(item.Key + ": " + item.Value[0])
continue
}
buffer.WriteString(item.Key + ": " + text.FgBlue.Sprint("Random") + "[\n")
for ii, v := range item.Value[:min(len(item.Value), displayLimit)] {
if ii == len(item.Value)-1 {
buffer.WriteString(indent + v + "\n")
} else {
buffer.WriteString(indent + v + ",\n")
}
}
// Add remaining values count if needed
if remainingValues := len(item.Value) - displayLimit; remainingValues > 0 {
buffer.WriteString(indent + text.FgGreen.Sprintf("+%d values", remainingValues) + "\n")
}
buffer.WriteString("]")
}
// Add remaining key-value pairs count if needed
if remainingPairs := len(headers) - displayLimit; remainingPairs > 0 {
buffer.WriteString(",\n" + text.FgGreen.Sprintf("+%d headers", remainingPairs))
}
return string(buffer.Bytes())
}
func (headers *Headers) UnmarshalJSON(b []byte) error {
var data []map[string]any
if err := json.Unmarshal(b, &data); err != nil {
return err
}
for _, item := range data {
for key, value := range item {
switch parsedValue := value.(type) {
case string:
*headers = append(*headers, KeyValue[string, []string]{Key: key, Value: []string{parsedValue}})
case []any:
parsedStr := make([]string, len(parsedValue))
for i, item := range parsedValue {
parsedStr[i] = fmt.Sprintf("%v", item)
}
*headers = append(*headers, KeyValue[string, []string]{Key: key, Value: parsedStr})
default:
return fmt.Errorf("unsupported type for headers expected string or []string, got %T", parsedValue)
}
}
}
return nil
}
func (headers *Headers) Set(value string) error {
parts := strings.SplitN(value, ":", 2)
switch len(parts) {
case 0:
headers.AppendByKey("", "")
case 1:
headers.AppendByKey(parts[0], "")
case 2:
headers.AppendByKey(parts[0], parts[1])
}
return nil
}
func (headers *Headers) AppendByKey(key, value string) {
if item := headers.GetValue(key); item != nil {
*item = append(*item, value)
} else {
*headers = append(*headers, KeyValue[string, []string]{Key: key, Value: []string{value}})
}
}
func (headers Headers) GetValue(key string) *[]string {
for i := range headers {
if headers[i].Key == key {
return &headers[i].Value
}
}
return nil
}

View File

@ -1,6 +0,0 @@
package types
type KeyValue[K comparable, V any] struct {
Key K
Value V
}

View File

@ -1,114 +0,0 @@
package types
import (
"bytes"
"encoding/json"
"fmt"
"strings"
"github.com/jedib0t/go-pretty/v6/text"
)
type Params []KeyValue[string, []string]
func (params Params) String() string {
var buffer bytes.Buffer
if len(params) == 0 {
return string(buffer.Bytes())
}
indent := " "
displayLimit := 3
for i, item := range params[:min(len(params), displayLimit)] {
if i > 0 {
buffer.WriteString(",\n")
}
if len(item.Value) == 1 {
buffer.WriteString(item.Key + ": " + item.Value[0])
continue
}
buffer.WriteString(item.Key + ": " + text.FgBlue.Sprint("Random") + "[\n")
for ii, v := range item.Value[:min(len(item.Value), displayLimit)] {
if ii == len(item.Value)-1 {
buffer.WriteString(indent + v + "\n")
} else {
buffer.WriteString(indent + v + ",\n")
}
}
// Add remaining values count if needed
if remainingValues := len(item.Value) - displayLimit; remainingValues > 0 {
buffer.WriteString(indent + text.FgGreen.Sprintf("+%d values", remainingValues) + "\n")
}
buffer.WriteString("]")
}
// Add remaining key-value pairs count if needed
if remainingPairs := len(params) - displayLimit; remainingPairs > 0 {
buffer.WriteString(",\n" + text.FgGreen.Sprintf("+%d params", remainingPairs))
}
return string(buffer.Bytes())
}
func (params *Params) UnmarshalJSON(b []byte) error {
var data []map[string]any
if err := json.Unmarshal(b, &data); err != nil {
return err
}
for _, item := range data {
for key, value := range item {
switch parsedValue := value.(type) {
case string:
*params = append(*params, KeyValue[string, []string]{Key: key, Value: []string{parsedValue}})
case []any:
parsedStr := make([]string, len(parsedValue))
for i, item := range parsedValue {
parsedStr[i] = fmt.Sprintf("%v", item)
}
*params = append(*params, KeyValue[string, []string]{Key: key, Value: parsedStr})
default:
return fmt.Errorf("unsupported type for params expected string or []string, got %T", parsedValue)
}
}
}
return nil
}
func (params *Params) Set(value string) error {
parts := strings.SplitN(value, "=", 2)
switch len(parts) {
case 0:
params.AppendByKey("", "")
case 1:
params.AppendByKey(parts[0], "")
case 2:
params.AppendByKey(parts[0], parts[1])
}
return nil
}
func (params *Params) AppendByKey(key, value string) {
if item := params.GetValue(key); item != nil {
*item = append(*item, value)
} else {
*params = append(*params, KeyValue[string, []string]{Key: key, Value: []string{value}})
}
}
func (params Params) GetValue(key string) *[]string {
for i := range params {
if params[i].Key == key {
return &params[i].Value
}
}
return nil
}

View File

@ -1,86 +0,0 @@
package types
import (
"bytes"
"encoding/json"
"fmt"
"net/url"
"github.com/jedib0t/go-pretty/v6/text"
)
type Proxies []url.URL
func (proxies Proxies) String() string {
var buffer bytes.Buffer
if len(proxies) == 0 {
return string(buffer.Bytes())
}
if len(proxies) == 1 {
buffer.WriteString(proxies[0].String())
return string(buffer.Bytes())
}
buffer.WriteString(text.FgBlue.Sprint("Random") + "[\n")
indent := " "
displayLimit := 5
for i, item := range proxies[:min(len(proxies), displayLimit)] {
if i > 0 {
buffer.WriteString(",\n")
}
buffer.WriteString(indent + item.String())
}
// Add remaining count if there are more items
if remainingValues := len(proxies) - displayLimit; remainingValues > 0 {
buffer.WriteString(",\n" + indent + text.FgGreen.Sprintf("+%d proxies", remainingValues))
}
buffer.WriteString("\n]")
return string(buffer.Bytes())
}
func (proxies *Proxies) UnmarshalJSON(b []byte) error {
var data any
if err := json.Unmarshal(b, &data); err != nil {
return err
}
switch v := data.(type) {
case string:
parsed, err := url.Parse(v)
if err != nil {
return err
}
*proxies = []url.URL{*parsed}
case []any:
var urls []url.URL
for _, item := range v {
url, err := url.Parse(item.(string))
if err != nil {
return err
}
urls = append(urls, *url)
}
*proxies = urls
default:
return fmt.Errorf("invalid type for Body: %T (should be URL or []URL)", v)
}
return nil
}
func (proxies *Proxies) Set(value string) error {
parsedURL, err := url.Parse(value)
if err != nil {
return err
}
*proxies = append(*proxies, *parsedURL)
return nil
}

View File

@ -1,44 +0,0 @@
package types
import (
"encoding/json"
"errors"
"net/url"
)
type RequestURL struct {
url.URL
}
func (requestURL *RequestURL) UnmarshalJSON(data []byte) error {
var urlStr string
if err := json.Unmarshal(data, &urlStr); err != nil {
return err
}
parsedURL, err := url.Parse(urlStr)
if err != nil {
return errors.New("Request URL is invalid")
}
requestURL.URL = *parsedURL
return nil
}
func (requestURL RequestURL) MarshalJSON() ([]byte, error) {
return json.Marshal(requestURL.URL.String())
}
func (requestURL RequestURL) String() string {
return requestURL.URL.String()
}
func (requestURL *RequestURL) Set(value string) error {
parsedURL, err := url.Parse(value)
if err != nil {
return err
}
requestURL.URL = *parsedURL
return nil
}

View File

@ -1,14 +0,0 @@
package utils
func IsNilOrZero[T comparable](value *T) bool {
if value == nil {
return true
}
var zero T
if *value == zero {
return true
}
return false
}

View File

@ -1,5 +1,54 @@
package utils package utils
func ToPtr[T any](value T) *T { import (
return &value "encoding/json"
"fmt"
"reflect"
)
func MarshalJSON(v any, maxSliceSize uint) string {
rv := reflect.ValueOf(v)
if rv.Kind() == reflect.Slice && rv.Len() == 0 {
return "[]"
}
data, err := json.MarshalIndent(truncateLists(v, int(maxSliceSize)), "", " ")
if err != nil {
return "{}"
}
return string(data)
}
func truncateLists(v interface{}, maxItems int) interface{} {
rv := reflect.ValueOf(v)
switch rv.Kind() {
case reflect.Slice, reflect.Array:
if rv.Len() > maxItems {
newSlice := reflect.MakeSlice(rv.Type(), maxItems, maxItems)
reflect.Copy(newSlice, rv.Slice(0, maxItems))
newSlice = reflect.Append(newSlice, reflect.ValueOf(fmt.Sprintf("...(%d more)", rv.Len()-maxItems)))
return newSlice.Interface()
}
case reflect.Map:
newMap := reflect.MakeMap(rv.Type())
for _, key := range rv.MapKeys() {
newMap.SetMapIndex(key, reflect.ValueOf(truncateLists(rv.MapIndex(key).Interface(), maxItems)))
}
return newMap.Interface()
case reflect.Struct:
newStruct := reflect.New(rv.Type()).Elem()
for i := 0; i < rv.NumField(); i++ {
newStruct.Field(i).Set(reflect.ValueOf(truncateLists(rv.Field(i).Interface(), maxItems)))
}
return newStruct.Interface()
case reflect.Ptr:
if rv.IsNil() {
return nil
}
return truncateLists(rv.Elem().Interface(), maxItems)
}
return v
} }

View File

@ -1,21 +0,0 @@
package utils
type Number interface {
int | int8 | int16 | int32 | int64
}
func NumLen[T Number](n T) T {
if n < 0 {
n = -n
}
if n == 0 {
return 1
}
var count T = 0
for n > 0 {
n /= 10
count++
}
return count
}

View File

@ -3,12 +3,46 @@ package utils
import ( import (
"fmt" "fmt"
"os" "os"
"github.com/jedib0t/go-pretty/v6/text"
) )
var Colors = struct {
reset string
Red string
Green string
Yellow string
Orange string
Blue string
Magenta string
Cyan string
Gray string
White string
}{
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) { func PrintErr(err error) {
fmt.Fprintln(os.Stderr, text.FgRed.Sprint(err.Error())) PrintlnC(Colors.Red, err.Error())
} }
func PrintErrAndExit(err error) { func PrintErrAndExit(err error) {

View File

@ -10,6 +10,15 @@ func Flatten[T any](nested [][]*T) []*T {
return flattened return flattened
} }
func Contains[T comparable](slice []T, item T) bool {
for _, i := range slice {
if i == item {
return true
}
}
return false
}
// RandomValueCycle returns a function that cycles through the provided slice of values // RandomValueCycle returns a function that cycles through the provided slice of values
// in a random order. Each call to the returned function will yield a value from the slice. // in a random order. Each call to the returned function will yield a value from the slice.
// The order of values is determined by the provided random number generator. // The order of values is determined by the provided random number generator.

View File

@ -2,13 +2,30 @@ package utils
import "time" import "time"
func DurationRoundBy(duration time.Duration, n int64) time.Duration { func MinDuration(durations ...time.Duration) time.Duration {
if durationLen := NumLen(duration.Nanoseconds()); durationLen > n { min := durations[0]
roundNum := 1 for _, d := range durations {
for range durationLen - n { if d < min {
roundNum *= 10 min = d
} }
return duration.Round(time.Duration(roundNum))
} }
return duration return min
}
func MaxDuration(durations ...time.Duration) time.Duration {
max := durations[0]
for _, d := range durations {
if d > max {
max = d
}
}
return max
}
func AvgDuration(durations ...time.Duration) time.Duration {
total := time.Duration(0)
for _, d := range durations {
total += d
}
return total / time.Duration(len(durations))
} }

59
utils/types.go Normal file
View File

@ -0,0 +1,59 @@
package utils
import (
"encoding/json"
"errors"
)
// Don't call this struct directly, use NewOption[T] or NewNoneOption[T] instead.
type Option[T any] struct {
// value holds the actual value of the Option if it is not None.
value T
// none indicates whether the Option is None (i.e., has no value).
none bool
}
func (o *Option[T]) IsNone() bool {
return o.none
}
// The returned value can be nil, if the Option is None, it will return nil and an error.
func (o *Option[T]) ValueOrErr() (*T, error) {
if o.IsNone() {
return nil, errors.New("Option is None")
}
return &o.value, nil
}
// The returned value can't be nil, if the Option is None, it will return the default value.
func (o *Option[T]) ValueOr(def *T) *T {
if o.IsNone() {
return def
}
return &o.value
}
// The returned value can't be nil, if the Option is None, it will panic.
func (o *Option[T]) ValueOrPanic() *T {
if o.IsNone() {
panic("Option is None")
}
return &o.value
}
func (o *Option[T]) UnmarshalJSON(data []byte) error {
if string(data) == "null" {
o.none = true
return nil
}
o.none = false
return json.Unmarshal(data, &o.value)
}
func NewOption[T any](value T) Option[T] {
return Option[T]{value: value}
}
func NewNoneOption[T any]() Option[T] {
return Option[T]{none: true}
}

59
validation/validator.go Normal file
View File

@ -0,0 +1,59 @@
package validation
import (
"reflect"
"strings"
"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(
"proxy_url",
func(fl validator.FieldLevel) bool {
url := fl.Field().String()
if url == "" {
return false
}
if err := validation.Var(url, "url"); err != nil {
return false
}
if !(url[:7] == "http://" ||
url[:9] == "socks5://" ||
url[:10] == "socks5h://") {
return false
}
return true
},
)
return validation
}