Compare commits

..

No commits in common. "main" and "v0.5.2" have entirely different histories.
main ... v0.5.2

44 changed files with 1527 additions and 2198 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@v7
with:
version: v2.0.2
args: --timeout=10m --config=.golangci.yml

View File

@ -1,33 +0,0 @@
version: "2"
run:
go: "1.24"
concurrency: 8
timeout: 10m
linters:
default: none
enable:
- asasalint
- asciicheck
- errcheck
- gomodguard
- goprintffuncname
- govet
- ineffassign
- misspell
- nakedret
- nolintlint
- prealloc
- reassign
- staticcheck
- unconvert
- unused
- whitespace
settings:
staticcheck:
checks:
- "all"
- "-S1002"
- "-ST1000"

View File

@ -1,17 +1,19 @@
FROM golang:1.24-alpine AS builder FROM golang:1.23.2-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
COPY . . COPY . .
RUN go build -ldflags "-s -w" -o dodo RUN go build -ldflags "-s -w" -o dodo
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 /dodo/config.json /dodo/config.json
ENTRYPOINT ["./dodo"] ENTRYPOINT ["./dodo", "-c", "/dodo/config.json"]

290
README.md
View File

@ -1,263 +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://ftp.aykhans.me/web/client/pubshares/hB6VSdCnBCr8gFPeiMuCji/browse?path=%2Fdodo.png">
</p> </p>
## Table of Contents
- [Installation](#installation)
- [Using Docker (Recommended)](#using-docker-recommended)
- [Using Pre-built Binaries](#using-pre-built-binaries)
- [Building from Source](#building-from-source)
- [Usage](#usage)
- [1. CLI Usage](#1-cli-usage)
- [2. Config File Usage](#2-config-file-usage)
- [2.1 JSON Example](#21-json-example)
- [2.2 YAML/YML Example](#22-yamlyml-example)
- [3. CLI & Config File Combination](#3-cli--config-file-combination)
- [Config Parameters Reference](#config-parameters-reference)
## Installation ## Installation
### With Docker (Recommended)
### Using Docker (Recommended) Pull the Dodo image from Docker Hub:
Pull the latest Dodo image from Docker Hub:
```sh ```sh
docker pull aykhans/dodo:latest docker pull aykhans/dodo:latest
``` ```
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.
To use Dodo with Docker and a local config file, mount the config file as a volume and pass it as an argument: ### With Binary File
You can grab binaries in the [releases](https://github.com/aykhans/dodo/releases) section.
### Build from Source
To build Dodo from source, you need to have [Go1.22+](https://golang.org/dl/) installed. <br>
Follow the steps below to build dodo:
1. **Clone the repository:**
```sh ```sh
docker run -v /path/to/config.json:/config.json aykhans/dodo -f /config.json git clone https://github.com/aykhans/dodo.git
``` ```
If you're using a remote config file via URL, you don't need to mount a volume: 2. **Navigate to the project directory:**
```sh ```sh
docker run aykhans/dodo -f https://raw.githubusercontent.com/aykhans/dodo/main/config.yaml cd dodo
``` ```
### Using Pre-built Binaries 3. **Build the project:**
Download the latest binaries from the [releases](https://github.com/aykhans/dodo/releases) section.
### Building from Source
To build Dodo from source, ensure you have [Go 1.24+](https://golang.org/dl/) installed.
```sh ```sh
go install -ldflags "-s -w" github.com/aykhans/dodo@latest go build -ldflags "-s -w" -o dodo
``` ```
This will generate an executable named `dodo` in the project directory.
## Usage ## 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.
Dodo supports CLI arguments, configuration files (JSON/YAML), or a combination of both. If both are used, CLI arguments take precedence. ### 1. CLI
Send 1000 GET requests to https://example.com with 10 parallel dodos (threads) and a timeout of 2000 milliseconds:
### 1. CLI Usage
Send 1000 GET requests to https://example.com with 10 parallel dodos (threads), each with a timeout of 2 seconds, within a maximum duration of 1 minute:
```sh ```sh
dodo -u https://example.com -m GET -d 10 -r 1000 -o 1m -t 2s dodo -u https://example.com -m GET -d 10 -r 1000 -t 2000
``` ```
With Docker: With Docker:
```sh ```sh
docker run --rm -i aykhans/dodo -u https://example.com -m GET -d 10 -r 1000 -o 1m -t 2s docker run --rm aykhans/dodo -u https://example.com -m GET -d 10 -r 1000 -t 2000
``` ```
### 2. Config File Usage ### 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:
Send 1000 GET requests to https://example.com with 10 parallel dodos (threads), each with a timeout of 800 milliseconds, within a maximum duration of 250 seconds: ```json
#### 2.1 JSON Example
```jsonc
{ {
"method": "GET", "method": "GET",
"url": "https://example.com", "url": "https://example.com",
"yes": false, "no_proxy_check": false,
"timeout": "800ms", "timeout": 2000,
"dodos": 10, "dodos_count": 10,
"requests": 1000, "request_count": 1000,
"duration": "250s", "params": {},
"headers": {},
"params": [ "cookies": {},
// A random value will be selected from the list for first "key1" param on each request "body": [""],
// And always "value" for second "key1" param on each request "proxies": [
// e.g. "?key1=value2&key1=value" {
{ "key1": ["value1", "value2", "value3", "value4"] }, "url": "http://example.com:8080",
{ "key1": "value" }, "username": "username",
"password": "password"
// A random value will be selected from the list for param "key2" on each request },
// e.g. "?key2=value2" {
{ "key2": ["value1", "value2"] }, "url": "http://example.com:8080"
], }
]
"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
// e.g. "key1: value3", "key1: value"
{ "key1": ["value1", "value2", "value3", "value4"] },
{ "key1": "value" },
// A random value will be selected from the list for header "key2" on each request
// e.g. "key2: value2"
{ "key2": ["value1", "value2"] },
],
"cookies": [
// A random value will be selected from the list for first "key1" cookie on each request
// And always "value" for second "key1" cookie on each request
// e.g. "key1=value4; key1=value"
{ "key1": ["value1", "value2", "value3", "value4"] },
{ "key1": "value" },
// A random value will be selected from the list for cookie "key2" on each request
// 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",
],
} }
``` ```
Send 1000 GET requests to https://example.com with 10 parallel dodos (threads) and a timeout of 2000 milliseconds:
```sh ```sh
dodo -f /path/config.json dodo -c /path/config.json
# OR
dodo -f https://example.com/config.json
``` ```
With Docker: With Docker:
```sh
docker run --rm -v ./path/config.json:/dodo/config.json -i aykhans/dodo
```
### 3. Both (CLI & JSON)
Override the config file arguments with CLI arguments:
```sh ```sh
docker run --rm -i -v /path/to/config.json:/config.json aykhans/dodo dodo -c /path/config.json -u https://example.com -m GET -d 10 -r 1000 -t 2000
# OR
docker run --rm -i aykhans/dodo -f https://example.com/config.json
``` ```
#### 2.2 YAML/YML Example
```yaml
method: "GET"
url: "https://example.com"
yes: false
timeout: "800ms"
dodos: 10
requests: 1000
duration: "250s"
params:
# 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"
- key1: ["value1", "value2", "value3", "value4"]
- key1: "value"
# A random value will be selected from the list for param "key2" on each request
# e.g. "?key2=value2"
- key2: ["value1", "value2"]
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
# e.g. "key1: value3", "key1: value"
- key1: ["value1", "value2", "value3", "value4"]
- key1: "value"
# A random value will be selected from the list for header "key2" on each request
# e.g. "key2: value2"
- key2: ["value1", "value2"]
cookies:
# A random value will be selected from the list for first "key1" cookie on each request
# And always "value" for second "key1" cookie on each request
# e.g. "key1=value4; key1=value"
- key1: ["value1", "value2", "value3", "value4"]
- key1: "value"
# A random value will be selected from the list for cookie "key2" on each request
# 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.yaml
# OR
dodo -f https://example.com/config.yaml
```
With Docker: With Docker:
```sh ```sh
docker run --rm -i -v /path/to/config.yaml:/config.yaml aykhans/dodo -f /config.yaml 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
# OR
docker run --rm -i aykhans/dodo -f https://example.com/config.yaml
``` ```
### 3. CLI & Config File Combination ## CLI and JSON Config Parameters
If the Headers, Params, Cookies and Body fields have multiple values, each request will choose a random value from the list.
CLI arguments override config file values: | Parameter | JSON config file | CLI Flag | CLI Short Flag | Type | Description | Default |
| --------------------- | ---------------- | --------------- | -------------- | -------------------------------- | ------------------------------------------------------------------- | ----------- |
```sh | Config file | - | --config-file | -c | String | Path to the JSON config file | - |
dodo -f /path/to/config.yaml -u https://example.com -m GET -d 10 -r 1000 -o 1m -t 5s | 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 |
With Docker: | Request count | request_count | --request-count | -r | Integer | Total number of requests to send | 1000 |
| Dodos count (Threads) | dodos_count | --dodos-count | -d | Integer | Number of dodos (threads) to send requests in parallel | 1 |
```sh | Timeout | timeout | --timeout | -t | Integer | Timeout for canceling each request (milliseconds) | 10000 |
docker run --rm -i -v /path/to/config.json:/config.json aykhans/dodo -f /config.json -u https://example.com -m GET -d 10 -r 1000 -o 1m -t 5s | No Proxy Check | no_proxy_check | --no-proxy-check| - | Boolean | Disable proxy check | false |
``` | Params | params | - | - | Key-Value {String: [String]} | Request parameters | - |
| Headers | headers | - | - | Key-Value {String: [String]} | Request headers | - |
## Config Parameters Reference | Cookies | cookies | - | - | Key-Value {String: [String]} | Request cookies | - |
| Body | body | - | - | [String] | Request body | - |
If `Headers`, `Params`, `Cookies`, `Body`, or `Proxy` fields have multiple values, each request will choose a random value from the list. | Proxy | proxies | - | - | List[Key-Value {string: string}] | List of proxies (will check active proxies before sending requests) | - |
| Parameter | 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 |
| Dodos (Threads) | dodos | -dodos | -d | UnsignedInteger | Number of dodos (threads) to send requests in parallel | 1 |
| Requests | requests | -requests | -r | UnsignedInteger | Total number of requests to send | - |
| Duration | duration | -duration | -o | Time | Maximum duration for the test | - |
| Timeout | timeout | -timeout | -t | Time | 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 | - |

View File

@ -1,45 +0,0 @@
# https://taskfile.dev
version: "3"
vars:
PLATFORMS:
- os: darwin
archs: [amd64, arm64]
- os: freebsd
archs: [386, amd64, arm]
- os: linux
archs: [386, amd64, arm, arm64]
- os: netbsd
archs: [386, amd64, arm]
- os: openbsd
archs: [386, amd64, arm, arm64]
- os: windows
archs: [386, amd64, arm64]
tasks:
run: go run main.go
fmt: gofmt -w -d .
lint: golangci-lint run
build: go build -ldflags "-s -w" -o "dodo"
build-all:
silent: true
cmds:
- rm -rf binaries
- |
{{ $ext := "" }}
{{- range $platform := .PLATFORMS }}
{{- if eq $platform.os "windows" }}
{{ $ext = ".exe" }}
{{- end }}
{{- range $arch := $platform.archs }}
echo "Building for {{$platform.os}}/{{$arch}}"
GOOS={{$platform.os}} GOARCH={{$arch}} go build -ldflags "-s -w" -o "./binaries/dodo-{{$platform.os}}-{{$arch}}{{$ext}}"
{{- end }}
{{- end }}
- echo -e "\033[32m*** Build completed ***\033[0m"

32
build.sh Executable file
View File

@ -0,0 +1,32 @@
#!/bin/bash
platforms=(
"darwin,amd64"
"darwin,arm64"
"freebsd,386"
"freebsd,amd64"
"freebsd,arm"
"linux,386"
"linux,amd64"
"linux,arm"
"linux,arm64"
"netbsd,386"
"netbsd,amd64"
"netbsd,arm"
"openbsd,386"
"openbsd,amd64"
"openbsd,arm"
"openbsd,arm64"
"windows,386"
"windows,amd64"
"windows,arm64"
)
for platform in "${platforms[@]}"; do
IFS=',' read -r build_os build_arch <<< "$platform"
ext=""
if [ "$build_os" == "windows" ]; then
ext=".exe"
fi
GOOS="$build_os" GOARCH="$build_arch" go build -ldflags "-s -w" -o "./binaries/dodo-$build_os-$build_arch$ext"
done

View File

@ -1,36 +1,22 @@
{ {
"method": "GET", "method": "GET",
"url": "https://example.com", "url": "https://example.com",
"yes": false, "no_proxy_check": true,
"timeout": "5s", "timeout": 10000,
"dodos": 8, "dodos_count": 50,
"requests": 1000, "request_count": 1000,
"duration": "10s", "params": {},
"headers": {},
"params": [ "cookies": {},
{ "key1": ["value1", "value2", "value3", "value4"] }, "body": [""],
{ "key1": "value" }, "proxies": [
{ "key2": ["value1", "value2"] } {
], "url": "http://example.com:8080",
"username": "username",
"headers": [ "password": "password"
{ "key1": ["value1", "value2", "value3", "value4"] }, },
{ "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 @@
method: "GET"
url: "https://example.com"
yes: false
timeout: "5s"
dodos: 8
requests: 1000
duration: "10s"
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
# 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"

View File

@ -1,181 +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:
dodo -u https://example.com -o 1m
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 -o 3m -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
-o, -duration Time Maximum duration for the test (e.g. 30s, 1m, 5h)
-t, -timeout Time 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,
DefaultTimeout,
DefaultMethod,
)
}
var (
version = false
configFile = ""
yes = false
method = ""
url types.RequestURL
dodosCount = uint(0)
requestCount = uint(0)
timeout time.Duration
duration 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(&duration, "duration", 0, "Maximum duration of the test")
flag.DurationVar(&duration, "o", 0, "Maximum duration of the test")
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 "duration", "o":
config.Duration = &types.Duration{Duration: duration}
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,225 +1,194 @@
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.3" VERSION string = "0.5.2"
DefaultUserAgent string = "Dodo/" + VERSION DefaultUserAgent string = "Dodo/" + VERSION
ProxyCheckURL string = "https://www.google.com"
DefaultMethod string = "GET" DefaultMethod string = "GET"
DefaultTimeout time.Duration = time.Second * 10 DefaultTimeout uint32 = 10000 // Milliseconds (10 seconds)
DefaultDodosCount uint = 1 DefaultDodosCount uint = 1
DefaultRequestCount uint = 0 DefaultRequestCount uint = 1000
DefaultDuration time.Duration = 0 MaxDodosCountForProxies uint = 20 // Max dodos count for proxy check
DefaultYes bool = false
) )
var SupportedProxySchemes []string = []string{"http", "socks5", "socks5h"}
type RequestConfig struct { type RequestConfig struct {
Method string Method string
URL url.URL URL *url.URL
Timeout time.Duration Timeout time.Duration
DodosCount uint DodosCount uint
RequestCount uint RequestCount uint
Duration time.Duration Params map[string][]string
Headers map[string][]string
Cookies map[string][]string
Proxies []Proxy
Body []string
Yes bool Yes bool
Params types.Params NoProxyCheck bool
Headers types.Headers
Cookies types.Cookies
Body types.Body
Proxies types.Proxies
} }
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,
Duration: conf.Duration.Duration,
Yes: *conf.Yes,
Params: conf.Params,
Headers: conf.Headers,
Cookies: conf.Cookies,
Body: conf.Body,
Proxies: conf.Proxies,
}
}
func (rc *RequestConfig) GetValidDodosCountForRequests() uint {
if rc.RequestCount == 0 {
return rc.DodosCount
}
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},
}) })
newHeaders := make(map[string][]string)
newHeaders["User-Agent"] = []string{DefaultUserAgent}
for k, v := range config.Headers {
newHeaders[k] = v
}
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()
if rc.RequestCount > 0 { t.AppendRow(table.Row{"Requests", config.RequestCount})
t.AppendRow(table.Row{"Requests", rc.RequestCount})
} else {
t.AppendRow(table.Row{"Requests"})
}
t.AppendSeparator() t.AppendSeparator()
if rc.Duration > 0 { t.AppendRow(table.Row{"Params", utils.MarshalJSON(config.Params, 3)})
t.AppendRow(table.Row{"Duration", rc.Duration})
} else {
t.AppendRow(table.Row{"Duration"})
}
t.AppendSeparator() t.AppendSeparator()
t.AppendRow(table.Row{"Params", rc.Params.String()}) t.AppendRow(table.Row{"Headers", utils.MarshalJSON(newHeaders, 3)})
t.AppendSeparator() t.AppendSeparator()
t.AppendRow(table.Row{"Headers", rc.Headers.String()}) t.AppendRow(table.Row{"Cookies", utils.MarshalJSON(config.Cookies, 3)})
t.AppendSeparator() t.AppendSeparator()
t.AppendRow(table.Row{"Cookies", rc.Cookies.String()}) t.AppendRow(table.Row{"Proxies Count", len(config.Proxies)})
t.AppendSeparator() t.AppendSeparator()
t.AppendRow(table.Row{"Proxy", rc.Proxies.String()}) t.AppendRow(table.Row{"Proxy Check", !config.NoProxyCheck})
t.AppendSeparator() t.AppendSeparator()
t.AppendRow(table.Row{"Body", rc.Body.String()}) t.AppendRow(table.Row{"Body", utils.MarshalJSON(config.Body, 3)})
t.Render() t.Render()
} }
type Config struct { func (config *RequestConfig) GetValidDodosCountForRequests() uint {
Method *string `json:"method" yaml:"method"` return min(config.DodosCount, config.RequestCount)
URL *types.RequestURL `json:"url" yaml:"url"`
Timeout *types.Timeout `json:"timeout" yaml:"timeout"`
DodosCount *uint `json:"dodos" yaml:"dodos"`
RequestCount *uint `json:"requests" yaml:"requests"`
Duration *types.Duration `json:"duration" yaml:"duration"`
Yes *bool `json:"yes" yaml:"yes"`
Params types.Params `json:"params" yaml:"params"`
Headers types.Headers `json:"headers" yaml:"headers"`
Cookies types.Cookies `json:"cookies" yaml:"cookies"`
Body types.Body `json:"body" yaml:"body"`
Proxies types.Proxies `json:"proxy" yaml:"proxy"`
} }
func NewConfig() *Config { func (config *RequestConfig) GetValidDodosCountForProxies() uint {
return &Config{} return min(config.DodosCount, uint(len(config.Proxies)), MaxDodosCountForProxies)
} }
func (config *Config) Validate() []error { func (config *RequestConfig) GetMaxConns(minConns uint) uint {
var errs []error maxConns := max(
if utils.IsNilOrZero(config.URL) { minConns, uint(config.GetValidDodosCountForRequests()),
errs = append(errs, errors.New("request URL is required"))
} else {
if config.URL.Scheme == "" {
config.URL.Scheme = "http"
}
if config.URL.Scheme != "http" && config.URL.Scheme != "https" {
errs = append(errs, errors.New("request URL scheme must be http or https"))
}
urlParams := types.Params{}
for key, values := range config.URL.Query() {
for _, value := range values {
urlParams = append(urlParams, types.KeyValue[string, []string]{
Key: key,
Value: []string{value},
})
}
}
config.Params = append(urlParams, config.Params...)
config.URL.RawQuery = ""
}
if utils.IsNilOrZero(config.Method) {
errs = append(errs, errors.New("request method is required"))
}
if utils.IsNilOrZero(config.Timeout) {
errs = append(errs, errors.New("request timeout must be greater than 0"))
}
if utils.IsNilOrZero(config.DodosCount) {
errs = append(errs, errors.New("dodos count must be greater than 0"))
}
if utils.IsNilOrZero(config.Duration) && utils.IsNilOrZero(config.RequestCount) {
errs = append(errs, errors.New("you should provide at least one of duration or request count"))
}
for i, proxy := range config.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 ((maxConns * 50 / 100) + maxConns)
}
type Config struct {
Method string `json:"method" validate:"http_method"` // custom validations: http_method
URL string `json:"url" validate:"http_url,required"`
Timeout uint32 `json:"timeout" validate:"gte=1,lte=100000"`
DodosCount uint `json:"dodos_count" validate:"gte=1"`
RequestCount uint `json:"request_count" validation_name:"request-count" validate:"gte=1"`
NoProxyCheck utils.Option[bool] `json:"no_proxy_check"`
}
func NewConfig(
method string,
timeout uint32,
dodosCount uint,
requestCount uint,
noProxyCheck utils.Option[bool],
) *Config {
if noProxyCheck == nil {
noProxyCheck = utils.NewNoneOption[bool]()
}
return &Config{
Method: method,
Timeout: timeout,
DodosCount: dodosCount,
RequestCount: requestCount,
NoProxyCheck: noProxyCheck,
} }
} }
return errs func (config *Config) MergeConfigs(newConfig *Config) {
} if newConfig.Method != "" {
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.Duration != nil { if !newConfig.NoProxyCheck.IsNone() {
config.Duration = newConfig.Duration config.NoProxyCheck = newConfig.NoProxyCheck
} }
if newConfig.Yes != nil {
config.Yes = newConfig.Yes
} }
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 NewJSONConfig(
config *Config,
params map[string][]string,
headers map[string][]string,
cookies map[string][]string,
proxies []Proxy,
body []string,
) *JSONConfig {
return &JSONConfig{
config, params, headers, cookies, proxies, 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
} }
@ -237,24 +206,25 @@ 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 NewCLIConfig(
config *Config,
yes bool,
configFile string,
) *CLIConfig {
return &CLIConfig{
config, yes, configFile,
} }
if config.DodosCount == nil {
config.DodosCount = utils.ToPtr(DefaultDodosCount)
} }
if config.RequestCount == nil {
config.RequestCount = utils.ToPtr(DefaultRequestCount) func (config *CLIConfig) MergeConfigs(newConfig *CLIConfig) {
config.Config.MergeConfigs(newConfig.Config)
if newConfig.ConfigFile != "" {
config.ConfigFile = newConfig.ConfigFile
} }
if config.Duration == nil {
config.Duration = &types.Duration{Duration: DefaultDuration}
}
if config.Yes == nil {
config.Yes = utils.ToPtr(DefaultYes)
}
config.Headers.SetIfNotExists("User-Agent", DefaultUserAgent)
} }

View File

@ -1,84 +0,0 @@
package config
import (
"encoding/json"
"errors"
"fmt"
"io"
"net/http"
"os"
"slices"
"strings"
"time"
"github.com/aykhans/dodo/types"
"gopkg.in/yaml.v3"
)
var supportedFileTypes = []string{"json", "yaml", "yml"}
func (config *Config) ReadFile(filePath types.ConfigFile) error {
var (
data []byte
err error
)
fileExt := filePath.Extension()
if slices.Contains(supportedFileTypes, fileExt) {
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 func() { _ = 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())
}
}
switch fileExt {
case "json":
return parseJSONConfig(data, config)
case "yml", "yaml":
return parseYAMLConfig(data, config)
}
}
return fmt.Errorf("unsupported config file type (supported types: %v)", strings.Join(supportedFileTypes, ", "))
}
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
}
func parseYAMLConfig(data []byte, config *Config) error {
err := yaml.Unmarshal(data, &config)
if err != nil {
return fmt.Errorf("YAML 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"
}

28
go.mod
View File

@ -1,21 +1,29 @@
module github.com/aykhans/dodo module github.com/aykhans/dodo
go 1.24.0 go 1.23.2
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.60.0 github.com/jedib0t/go-pretty/v6 v6.6.2
gopkg.in/yaml.v3 v3.0.1 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/klauspost/compress v1.18.0 // indirect github.com/gabriel-vasile/mimetype v1.4.4 // indirect
github.com/mattn/go-runewidth v0.0.16 // 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/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.38.0 // indirect golang.org/x/crypto v0.29.0 // indirect
golang.org/x/sys v0.31.0 // indirect golang.org/x/sys v0.27.0 // indirect
golang.org/x/term v0.30.0 // indirect golang.org/x/term v0.26.0 // indirect
golang.org/x/text v0.23.0 // indirect golang.org/x/text v0.20.0 // indirect
) )

59
go.sum
View File

@ -1,35 +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/klauspost/compress v1.18.0 h1:c/Cqfb0r+Yi+JtIEq73FWXVkRonBlf0CRNYc8Zttxdo= github.com/go-playground/assert/v2 v2.2.0 h1:JvknZsQTYeFEAhQwI4qEt9cyV5ONwRHC+lYKSsYSR8s=
github.com/klauspost/compress v1.18.0/go.mod h1:2Pp+KzxcywXVXMr50+X0Q/Lsb43OQHYWRCY2AiWywWQ= github.com/go-playground/assert/v2 v2.2.0/go.mod h1:VDjEfimB/XKnb+ZQfWdccd7VUvScMdVu0Titje2rxJ4=
github.com/mattn/go-runewidth v0.0.16 h1:E5ScNMtiwvlvB5paMFdw9p4kSQzbXFikJ5SQO6TULQc= github.com/go-playground/locales v0.14.1 h1:EWaQ/wswjilfKLTECiXz7Rh+3BjFhfDFKv/oXslEjJA=
github.com/mattn/go-runewidth v0.0.16/go.mod h1:Jdepj2loyihRzMpdS35Xk/zdY8IAYHsh153qUoGf23w= 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/go.mod h1:pMDklpSncoRMuLFrf1W9Ss9KT+0rH90U12bZKk7uwG0=
github.com/leodido/go-urn v1.4.0 h1:WT9HwE9SGECu3lg4d/dIA+jxlljEa1/ffXKmRjqdmIQ=
github.com/leodido/go-urn v1.4.0/go.mod h1:bvxc+MVxLKB4z00jd1z+Dvzr47oO32F/QSNjSBOlFxI=
github.com/mattn/go-runewidth v0.0.15 h1:UNAjwbU9l54TA3KzvqLGxwWjHmMgBUVhBiTjelZgg3U=
github.com/mattn/go-runewidth v0.0.15/go.mod h1:Jdepj2loyihRzMpdS35Xk/zdY8IAYHsh153qUoGf23w=
github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM= github.com/pmezard/go-difflib v1.0.0 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.60.0 h1:kBRYS0lOhVJ6V+bYN8PqAHELKHtXqwq9zNMLKx1MBsw= github.com/valyala/fasthttp v1.57.0 h1:Xw8SjWGEP/+wAAgyy5XTvgrWlOD1+TxbbvNADYCm1Tg=
github.com/valyala/fasthttp v1.60.0/go.mod h1:iY4kDgV3Gc6EqhRZ8icqcmlG6bqhcDXfuHgTO4FXCvc= 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.38.0 h1:vRMAPTMaeGqVhG5QyLJHqNDwecKTomGeqbnfZyKlBI8= golang.org/x/crypto v0.29.0 h1:L5SG1JTTXupVV3n6sUqMTeWbjAyfPwoda2DLX8J8FrQ=
golang.org/x/net v0.38.0/go.mod h1:ivrbrMbzFq5J41QOQh0siUuly180yBYtLp+CKbEaFx8= golang.org/x/crypto v0.29.0/go.mod h1:+F4F4N5hv6v38hfeYwTdx20oUvLLc+QfrE9Ax9HtgRg=
golang.org/x/sys v0.31.0 h1:ioabZlmFYtWhL+TRYpcnNlLwhyxaM9kWTDEmfnprqik= golang.org/x/net v0.31.0 h1:68CPQngjLL0r2AlUKiSxtQFKvzRVbnzLwMUn5SzcLHo=
golang.org/x/sys v0.31.0/go.mod h1:BJP2sWEmIv4KK5OTEluFJCKSidICx8ciO85XgH3Ak8k= golang.org/x/net v0.31.0/go.mod h1:P4fl1q7dY2hnZFxEk4pPSkDHF+QqjitcnDjUQyMM+pM=
golang.org/x/term v0.30.0 h1:PQ39fJZ+mfadBm0y5WlL4vlM7Sx1Hgf13sMIY2+QS9Y= golang.org/x/sys v0.27.0 h1:wBqf8DvsY9Y/2P8gAfPDEYNuS30J4lPHJxXSb/nJZ+s=
golang.org/x/term v0.30.0/go.mod h1:NYYFdzHoI5wRh/h5tDMdMqCqPJZEuNqVR5xJLd/n67g= golang.org/x/sys v0.27.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA=
golang.org/x/text v0.23.0 h1:D71I7dUrlY+VX0gQShAThNGHFxZ13dGLBHQLVl1mJlY= golang.org/x/term v0.26.0 h1:WEQa6V3Gja/BhNxg540hBip/kkaYtRg3cxg4oXSw4AU=
golang.org/x/text v0.23.0/go.mod h1:/BLNzu4aZCJ1+kcD0DNRotWKage4q2rGVAg4o22unh4= golang.org/x/term v0.26.0/go.mod h1:Si5m1o57C5nBNQo5z1iq+XDijt21BDBDp2bK0QI8e3E=
gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405 h1:yhCVgyC4o1eVCa2tZl7eS0r+SDo693bJlVdllGtEeKM= 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/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=

113
main.go
View File

@ -2,68 +2,117 @@ 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.NewConfig("", 0, 0, 0, nil)
jsonConf := config.NewJSONConfig(
config.NewConfig("", 0, 0, 0, nil), nil, nil, nil, nil, nil,
)
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 { if err != nil {
utils.PrintErrAndExit(err) 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)
}
if configFile.String() != "" { conf.MergeConfigs(cliConf.Config)
tempConf := config.NewConfig() conf.SetDefaults()
if err := tempConf.ReadFile(configFile); err != nil { if err := validator.Struct(conf); err != nil {
utils.PrintErrAndExit(
customerrors.ValidationErrorsFormater(
err.(goValidator.ValidationErrors),
),
)
}
parsedURL, err := url.Parse(conf.URL)
if err != nil {
utils.PrintErrAndExit(err) utils.PrintErrAndExit(err)
} }
tempConf.MergeConfig(conf) requestConf := &config.RequestConfig{
conf = tempConf Method: conf.Method,
URL: parsedURL,
Timeout: time.Duration(conf.Timeout) * time.Millisecond,
DodosCount: conf.DodosCount,
RequestCount: conf.RequestCount,
Params: jsonConf.Params,
Headers: jsonConf.Headers,
Cookies: jsonConf.Cookies,
Proxies: jsonConf.Proxies,
Body: jsonConf.Body,
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()
}

96
readers/cli.go Normal file
View File

@ -0,0 +1,96 @@
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.NewCLIConfig(config.NewConfig("", 0, 0, 0, nil), false, "")
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"
}

36
readers/json.go Normal file
View File

@ -0,0 +1,36 @@
package readers
import (
"encoding/json"
"os"
"github.com/aykhans/dodo/config"
customerrors "github.com/aykhans/dodo/custom_errors"
)
func JSONConfigReader(filePath string) (*config.JSONConfig, error) {
data, err := os.ReadFile(filePath)
if err != nil {
return nil, customerrors.OSErrorFormater(err)
}
jsonConf := config.NewJSONConfig(
config.NewConfig("", 0, 0, 0, nil),
nil, nil, nil, nil, nil,
)
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,13 +22,18 @@ 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 {
// If noProxyCheck is true, we will return the clients without checking the proxies.
if noProxyCheck {
clients := make([]*fasthttp.HostClient, 0, proxiesLen) clients := make([]*fasthttp.HostClient, 0, proxiesLen)
addr := URL.Host addr := URL.Host
if isTLS && URL.Port() == "" { if isTLS && URL.Port() == "" {
@ -53,6 +61,43 @@ func getClients(
return clients 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,
),
)
}
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{
MaxConns: int(maxConns), MaxConns: int(maxConns),
IsTLS: isTLS, IsTLS: isTLS,
@ -65,20 +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)
switch proxy.Scheme { var (
case "socks5", "socks5h": wg sync.WaitGroup
dialer = fasthttpproxy.FasthttpSocksDialerDualStack(proxy.String()) streamWG sync.WaitGroup
case "http": )
dialer = fasthttpproxy.FasthttpHTTPDialerDualStackTimeout(proxy.String(), timeout) wg.Add(dodosCountInt)
default: streamWG.Add(1)
return nil, errors.New("unsupported proxy scheme") 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 {
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.
@ -17,7 +18,7 @@ import (
func streamProgress( func streamProgress(
ctx context.Context, ctx context.Context,
wg *sync.WaitGroup, wg *sync.WaitGroup,
total uint, total int64,
message string, message string,
increase <-chan int64, increase <-chan int64,
) { ) {
@ -27,26 +28,19 @@ func streamProgress(
pw.SetStyle(progress.StyleBlocks) pw.SetStyle(progress.StyleBlocks)
pw.SetTrackerLength(40) pw.SetTrackerLength(40)
pw.SetUpdateFrequency(time.Millisecond * 250) pw.SetUpdateFrequency(time.Millisecond * 250)
if total == 0 {
pw.Style().Visibility.Percentage = false
}
go pw.Render() go pw.Render()
dodosTracker := progress.Tracker{ dodosTracker := progress.Tracker{
Message: message, Message: message,
Total: int64(total), Total: total,
} }
pw.AppendTracker(&dodosTracker) pw.AppendTracker(&dodosTracker)
for { for {
select { select {
case <-ctx.Done(): case <-ctx.Done():
if err := ctx.Err(); err == context.Canceled || err == context.DeadlineExceeded {
dodosTracker.MarkAsDone()
} else {
dodosTracker.MarkAsErrored()
}
time.Sleep(time.Millisecond * 300)
fmt.Printf("\r") fmt.Printf("\r")
dodosTracker.MarkAsErrored()
time.Sleep(time.Millisecond * 300)
pw.Stop()
return return
case value := <-increase: case value := <-increase:
@ -54,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,25 +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)
} }
} }
// 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.
@ -186,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() KeyValue { return keyValues }
return func() T { 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
Error error
Time time.Duration 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(
mergedResponses[response.Error.Error()],
response.Time,
)
} else {
mergedResponses[fmt.Sprintf("%d", response.StatusCode)] = append(
mergedResponses[fmt.Sprintf("%d", response.StatusCode)],
response.Time, response.Time,
) )
allDurations = append(allDurations, 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()
} }
if len(mergedResponses) > 1 {
t.AppendRow(table.Row{ t.AppendRow(table.Row{
"Total", "Total",
total.Count, totalCount,
utils.DurationRoundBy(total.Min, roundPrecision), totalMinDuration,
utils.DurationRoundBy(total.Max, roundPrecision), totalMaxDuration,
utils.DurationRoundBy(total.Sum/time.Duration(total.Count), roundPrecision), // Average totalDuration / time.Duration(totalCount),
utils.DurationRoundBy(total.P90, roundPrecision),
utils.DurationRoundBy(total.P95, roundPrecision),
utils.DurationRoundBy(total.P99, roundPrecision),
}) })
}
t.Render() t.Render()
} }

View File

@ -2,44 +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) {
if requestConfig.Duration > 0 { checkConnectionCtx, checkConnectionCtxCancel := context.WithTimeout(ctx, 8*time.Second)
var cancel context.CancelFunc if !checkConnection(checkConnectionCtx) {
ctx, cancel = context.WithTimeout(ctx, requestConfig.Duration) checkConnectionCtxCancel()
defer cancel() 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
@ -64,38 +72,28 @@ func releaseDodos(
wg sync.WaitGroup wg sync.WaitGroup
streamWG sync.WaitGroup streamWG sync.WaitGroup
requestCountPerDodo uint requestCountPerDodo uint
dodosCount = requestConfig.GetValidDodosCountForRequests() dodosCount uint = requestConfig.GetValidDodosCountForRequests()
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(int(dodosCount)) 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, requestConfig.RequestCount, "Dodos Working🔥", increase) go streamProgress(streamCtx, &streamWG, int64(requestCount), "Dodos Working🔥", increase)
if requestConfig.RequestCount == 0 {
for i := range dodosCount {
go sendRequest(
ctx,
newRequest(*requestConfig, clients, int64(i)),
requestConfig.Timeout,
&responses[i],
increase,
&wg,
)
}
} else {
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 sendRequestByCount( go sendRequest(
ctx, ctx,
newRequest(*requestConfig, clients, int64(i)), newRequest(*requestConfig, clients, int64(i)),
requestConfig.Timeout, requestConfig.Timeout,
@ -105,19 +103,17 @@ func releaseDodos(
&wg, &wg,
) )
} }
}
wg.Wait() wg.Wait()
streamCtxCancel() streamCtxCancel()
streamWG.Wait() streamWG.Wait()
return utils.Flatten(responses) return utils.Flatten(responses)
} }
// sendRequestByCount sends a specified number of HTTP requests concurrently with a given timeout. // sendRequest sends a specified number of HTTP requests concurrently with a given timeout.
// It appends the responses to the provided responseData slice and sends the count of completed requests // It appends the responses to the provided responseData slice and sends the count of completed requests
// to the increase channel. The function terminates early if the context is canceled or if a custom // to the increase channel. The function terminates early if the context is canceled or if a custom
// interrupt error is encountered. // interrupt error is encountered.
func sendRequestByCount( func sendRequest(
ctx context.Context, ctx context.Context,
request *Request, request *Request,
timeout time.Duration, timeout time.Duration,
@ -142,11 +138,12 @@ func sendRequestByCount(
} }
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,
Error: err,
Time: completedTime, Time: completedTime,
}) })
increase <- 1 increase <- 1
@ -154,54 +151,8 @@ func sendRequestByCount(
} }
*responseData = append(*responseData, &Response{ *responseData = append(*responseData, &Response{
Response: strconv.Itoa(response.StatusCode()), StatusCode: response.StatusCode(),
Time: completedTime, Error: nil,
})
increase <- 1
}()
}
}
// sendRequest continuously sends HTTP requests until the context is canceled.
// It records the response status code or error message along with the response time,
// and signals each completed request through the increase channel.
func sendRequest(
ctx context.Context,
request *Request,
timeout time.Duration,
responseData *[]*Response,
increase chan<- int64,
wg *sync.WaitGroup,
) {
defer wg.Done()
for {
if ctx.Err() != nil {
return
}
func() {
startTime := time.Now()
response, err := request.Send(ctx, timeout)
completedTime := time.Since(startTime)
if response != nil {
defer fasthttp.ReleaseResponse(response)
}
if err != nil {
if err == types.ErrInterrupt {
return
}
*responseData = append(*responseData, &Response{
Response: err.Error(),
Time: completedTime,
})
increase <- 1
return
}
*responseData = append(*responseData, &Response{
Response: strconv.Itoa(response.StatusCode()),
Time: completedTime, Time: completedTime,
}) })
increase <- 1 increase <- 1

View File

@ -1,94 +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 buffer.String()
}
if len(body) == 1 {
buffer.WriteString(body[0])
return buffer.String()
}
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 buffer.String()
}
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) UnmarshalYAML(unmarshal func(interface{}) error) error {
var data any
if err := unmarshal(&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,32 +0,0 @@
package types
import "strings"
type FileLocationType int
const (
FileLocationTypeLocal FileLocationType = iota
FileLocationTypeRemoteHTTP
)
type ConfigFile string
func (configFile ConfigFile) String() string {
return string(configFile)
}
func (configFile ConfigFile) LocationType() FileLocationType {
if strings.HasPrefix(string(configFile), "http://") || strings.HasPrefix(string(configFile), "https://") {
return FileLocationTypeRemoteHTTP
}
return FileLocationTypeLocal
}
func (configFile ConfigFile) Extension() string {
i := strings.LastIndex(configFile.String(), ".")
if i == -1 {
return ""
}
return configFile.String()[i+1:]
}

View File

@ -1,139 +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 buffer.String()
}
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 buffer.String()
}
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
}
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) UnmarshalYAML(unmarshal func(interface{}) error) error {
var raw []map[string]any
if err := unmarshal(&raw); err != nil {
return err
}
for _, param := range raw {
for key, value := range param {
switch parsed := value.(type) {
case string:
*cookies = append(*cookies, KeyValue[string, []string]{Key: key, Value: []string{parsed}})
case []any:
var values []string
for _, v := range parsed {
if str, ok := v.(string); ok {
values = append(values, str)
}
}
*cookies = append(*cookies, KeyValue[string, []string]{Key: key, Value: values})
}
}
}
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
}

View File

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

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,156 +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 buffer.String()
}
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 buffer.String()
}
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
}
func (headers Headers) Has(key string) bool {
for i := range headers {
if headers[i].Key == key {
return true
}
}
return false
}
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) UnmarshalYAML(unmarshal func(interface{}) error) error {
var raw []map[string]any
if err := unmarshal(&raw); err != nil {
return err
}
for _, param := range raw {
for key, value := range param {
switch parsed := value.(type) {
case string:
*headers = append(*headers, KeyValue[string, []string]{Key: key, Value: []string{parsed}})
case []any:
var values []string
for _, v := range parsed {
if str, ok := v.(string); ok {
values = append(values, str)
}
}
*headers = append(*headers, KeyValue[string, []string]{Key: key, Value: values})
}
}
}
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) SetIfNotExists(key string, value string) bool {
if headers.Has(key) {
return false
}
*headers = append(*headers, KeyValue[string, []string]{Key: key, Value: []string{value}})
return true
}

View File

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

View File

@ -1,139 +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 buffer.String()
}
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 buffer.String()
}
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
}
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) UnmarshalYAML(unmarshal func(interface{}) error) error {
var raw []map[string]any
if err := unmarshal(&raw); err != nil {
return err
}
for _, param := range raw {
for key, value := range param {
switch parsed := value.(type) {
case string:
*params = append(*params, KeyValue[string, []string]{Key: key, Value: []string{parsed}})
case []any:
var values []string
for _, v := range parsed {
if str, ok := v.(string); ok {
values = append(values, str)
}
}
*params = append(*params, KeyValue[string, []string]{Key: key, Value: values})
}
}
}
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
}

View File

@ -1,116 +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 buffer.String()
}
if len(proxies) == 1 {
buffer.WriteString(proxies[0].String())
return buffer.String()
}
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 buffer.String()
}
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) UnmarshalYAML(unmarshal func(interface{}) error) error {
var data any
if err := unmarshal(&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,59 +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) UnmarshalYAML(unmarshal func(interface{}) error) error {
var urlStr string
if err := unmarshal(&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,57 +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.String())
}
func (timeout *Timeout) UnmarshalYAML(unmarshal func(interface{}) error) error {
var v any
if err := unmarshal(&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)")
}
}

View File

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

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.
@ -20,9 +29,9 @@ func Flatten[T any](nested [][]*T) []*T {
// The returned function isn't thread-safe and should be used in a single-threaded context. // The returned function isn't thread-safe and should be used in a single-threaded context.
func RandomValueCycle[Value any](values []Value, localRand *rand.Rand) func() Value { func RandomValueCycle[Value any](values []Value, localRand *rand.Rand) func() Value {
var ( var (
clientsCount = len(values) clientsCount int = len(values)
currentIndex = localRand.Intn(clientsCount) currentIndex int = localRand.Intn(clientsCount)
stopIndex = currentIndex stopIndex int = currentIndex
) )
return func() Value { return func() Value {

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

87
utils/types.go Normal file
View File

@ -0,0 +1,87 @@
package utils
import (
"encoding/json"
"errors"
)
type NonNilT interface {
~int | ~float64 | ~string | ~bool
}
type Option[T NonNilT] interface {
IsNone() bool
ValueOrErr() (T, error)
ValueOr(def T) T
ValueOrPanic() T
UnmarshalJSON(data []byte) error
}
// Don't call this struct directly, use NewOption[T] or NewNoneOption[T] instead.
type option[T NonNilT] 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
}
// If the Option is None, it will return zero value of the type and an error.
func (o *option[T]) ValueOrErr() (T, error) {
if o.IsNone() {
return o.value, errors.New("Option is None")
}
return o.value, 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
}
// 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]) SetValue(value T) {
o.value = value
o.none = false
}
func (o *option[T]) SetNone() {
var zeroValue T
o.value = zeroValue
o.none = true
}
func (o *option[T]) UnmarshalJSON(data []byte) error {
if string(data) == "null" || len(data) == 0 {
o.SetNone()
return nil
}
if err := json.Unmarshal(data, &o.value); err != nil {
o.SetNone()
return err
}
o.none = false
return nil
}
func NewOption[T NonNilT](value T) *option[T] {
return &option[T]{value: value}
}
func NewNoneOption[T NonNilT]() *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
}