38 Commits

Author SHA1 Message Date
23c74bdbb1 Merge pull request #114 from aykhans/core/refactor
General refactor
2025-05-29 02:42:57 +04:00
addf92df91 🔧 Add 'skip-verify' parameter to skip SSL/TLS cert verification 2025-05-29 02:38:07 +04:00
6aeda3706b 💄 general formatting 2025-05-29 00:38:48 +04:00
dc1cd05714 Merge pull request #112 from aykhans/core/refactor-clients
🔧 Update client configs to skip connection verification if it is not secure
2025-05-25 19:18:03 +04:00
2b9d0520b0 🔧 Update client configs to skip connection verification if it is not secure 2025-05-25 19:16:30 +04:00
bea2e7c040 Merge pull request #111 from aykhans/dependabot/go_modules/github.com/valyala/fasthttp-1.62.0
Bump github.com/valyala/fasthttp from 1.61.0 to 1.62.0
2025-05-08 15:11:32 +04:00
b52b336a52 Bump github.com/valyala/fasthttp from 1.61.0 to 1.62.0
Bumps [github.com/valyala/fasthttp](https://github.com/valyala/fasthttp) from 1.61.0 to 1.62.0.
- [Release notes](https://github.com/valyala/fasthttp/releases)
- [Commits](https://github.com/valyala/fasthttp/compare/v1.61.0...v1.62.0)

---
updated-dependencies:
- dependency-name: github.com/valyala/fasthttp
  dependency-version: 1.62.0
  dependency-type: direct:production
  update-type: version-update:semver-minor
...

Signed-off-by: dependabot[bot] <support@github.com>
2025-05-08 00:48:52 +00:00
c927e31c49 Merge pull request #110 from aykhans/core/update-go
🔧 bump go version to 1.24.2
2025-04-26 16:38:03 +04:00
d8e6f532a8 🔧 bump go version to 1.24.2 2025-04-26 16:36:27 +04:00
cf5cd23d97 Merge pull request #109 from aykhans/dependabot/go_modules/github.com/valyala/fasthttp-1.61.0
All checks were successful
golangci-lint / lint (push) Successful in 2m31s
Bump github.com/valyala/fasthttp from 1.60.0 to 1.61.0
2025-04-23 15:10:41 +04:00
350ff4d66d Bump github.com/valyala/fasthttp from 1.60.0 to 1.61.0
Bumps [github.com/valyala/fasthttp](https://github.com/valyala/fasthttp) from 1.60.0 to 1.61.0.
- [Release notes](https://github.com/valyala/fasthttp/releases)
- [Commits](https://github.com/valyala/fasthttp/compare/v1.60.0...v1.61.0)

---
updated-dependencies:
- dependency-name: github.com/valyala/fasthttp
  dependency-version: 1.61.0
  dependency-type: direct:production
  update-type: version-update:semver-minor
...

Signed-off-by: dependabot[bot] <support@github.com>
2025-04-23 00:25:31 +00:00
cb8898d20e Merge pull request #108 from aykhans/core/refactor-duration-logic
All checks were successful
golangci-lint / lint (push) Successful in 3m4s
🔨 Replace 'time.AfterFunc' with 'context.WithTimeout'
2025-04-18 15:42:21 +04:00
a552d1c9f9 🔨 Replace 'time.AfterFunc' with 'context.WithTimeout' 2025-04-18 15:39:17 +04:00
35263f1dd6 Merge pull request #106 from aykhans/bump/version
All checks were successful
golangci-lint / lint (push) Successful in 3m0s
⬆️ bump version to 0.6.3
2025-04-03 05:25:06 +04:00
930e173a6a ⬆️ bump version to 0.6.3 2025-04-03 05:24:11 +04:00
bea2a81afa Merge pull request #104 from aykhans/chore/refactor-ci
🔧 Refactor .golangci and Taskfile
2025-04-03 05:05:50 +04:00
53ed486b23 Merge pull request #105 from aykhans/dependabot/go_modules/github.com/valyala/fasthttp-1.60.0
Bump github.com/valyala/fasthttp from 1.59.0 to 1.60.0
2025-04-03 05:05:02 +04:00
0b9c32a09d Merge branch 'main' into chore/refactor-ci 2025-04-03 05:04:12 +04:00
42d5617e3f 🎨 Format files 2025-04-03 05:01:22 +04:00
e80ae9ab24 🔧 bump golangci version 2025-04-03 04:24:23 +04:00
86a6f7814b Bump github.com/valyala/fasthttp from 1.59.0 to 1.60.0
Bumps [github.com/valyala/fasthttp](https://github.com/valyala/fasthttp) from 1.59.0 to 1.60.0.
- [Release notes](https://github.com/valyala/fasthttp/releases)
- [Commits](https://github.com/valyala/fasthttp/compare/v1.59.0...v1.60.0)

---
updated-dependencies:
- dependency-name: github.com/valyala/fasthttp
  dependency-version: 1.60.0
  dependency-type: direct:production
  update-type: version-update:semver-minor
...

Signed-off-by: dependabot[bot] <support@github.com>
2025-04-03 00:20:47 +00:00
09034b5f9e 🔧 bump golangci/golangci-lint-action version 2025-04-03 04:18:53 +04:00
f1ca2041c3 🔧 bump golangci version 2025-04-03 04:15:48 +04:00
f5a29a2657 Merge pull request #103 from aykhans/chore/general-refactoring
General refactoring
2025-04-03 04:11:38 +04:00
439f66eb87 🔧 Refactor .golangci and Taskfile 2025-04-03 03:07:38 +04:00
415d0130ce 🔨 Move duration cancel logic to requests package 2025-04-01 21:10:02 +04:00
abaa8e90b2 🔧 Add 'run' and 'fmt' commands to Taskfile 2025-04-01 21:09:17 +04:00
046ce74cd9 Merge pull request #102 from aykhans/chore/replace-makefile-with-taskfile
All checks were successful
golangci-lint / lint (push) Successful in 2m36s
🔧 Replace Makefile with Taskfile and remove 'build.sh'
2025-04-01 20:01:35 +04:00
681cafc213 🔧 Replace Makefile with Taskfile and remove 'build.sh' 2025-03-27 23:32:41 +04:00
7e05cf4f6b Merge pull request #101 from aykhans/feat/add-duration
All checks were successful
golangci-lint / lint (push) Successful in 23s
 Add duration
2025-03-24 18:12:10 +04:00
934cd0ad33 📚 Update docs 2025-03-24 17:02:58 +04:00
69c4841a05 📚 Update docs 2025-03-24 17:02:06 +04:00
3cc165cbf4 ⬆️ bump version to 0.6.2 2025-03-24 16:55:23 +04:00
59f40ad825 Add duration 2025-03-24 16:54:09 +04:00
a170588574 Merge pull request #100 from aykhans/refactor/config
All checks were successful
golangci-lint / lint (push) Successful in 25s
🔨 Add 'User-Agent' in 'SetDefaults' function
2025-03-23 21:19:31 +04:00
2a0ac390d8 🔨 Add 'User-Agent'in 'SetDefaults' function 2025-03-22 22:25:54 +04:00
11bb8b3fb0 Merge pull request #99 from aykhans/docs/update-readme
All checks were successful
golangci-lint / lint (push) Successful in 21s
📚 Update README.md
2025-03-20 19:23:41 +04:00
1aadc3419a 📚 Update README.md 2025-03-20 19:23:14 +04:00
27 changed files with 440 additions and 225 deletions

View File

@ -19,7 +19,7 @@ jobs:
with: with:
go-version: stable go-version: stable
- name: golangci-lint - name: golangci-lint
uses: golangci/golangci-lint-action@v6 uses: golangci/golangci-lint-action@v7
with: with:
version: v1.64 version: v2.0.2
args: --timeout=10m --config=.golangci.yml args: --timeout=10m --config=.golangci.yml

View File

@ -1,16 +1,16 @@
version: "2"
run: run:
go: "1.24" go: "1.24"
concurrency: 8 concurrency: 8
timeout: 10m timeout: 10m
linters: linters:
disable-all: true default: none
enable: enable:
- asasalint - asasalint
- asciicheck - asciicheck
- errcheck - errcheck
- gofmt
- goimports
- gomodguard - gomodguard
- goprintffuncname - goprintffuncname
- govet - govet
@ -21,7 +21,13 @@ linters:
- prealloc - prealloc
- reassign - reassign
- staticcheck - staticcheck
- typecheck
- unconvert - unconvert
- unused - unused
- whitespace - whitespace
settings:
staticcheck:
checks:
- "all"
- "-S1002"
- "-ST1000"

View File

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

View File

@ -45,51 +45,35 @@ Download the latest binaries from the [releases](https://github.com/aykhans/dodo
### Building from Source ### Building from Source
To build Dodo from source, ensure you have [Go 1.24+](https://golang.org/dl/) installed. Then follow these steps: To build Dodo from source, ensure you have [Go 1.24+](https://golang.org/dl/) installed.
1. **Clone the repository:**
```sh ```sh
git clone https://github.com/aykhans/dodo.git go install -ldflags "-s -w" github.com/aykhans/dodo@latest
``` ```
2. **Navigate to the project directory:**
```sh
cd dodo
```
3. **Build the project:**
```sh
go build -ldflags "-s -w" -o dodo
```
This will generate an executable named `dodo` in the project directory.
## Usage ## Usage
Dodo supports CLI arguments, configuration files (JSON/YAML), or a combination of both. If both are used, CLI arguments take precedence. Dodo supports CLI arguments, configuration files (JSON/YAML), or a combination of both. If both are used, CLI arguments take precedence.
### 1. CLI Usage ### 1. CLI Usage
Send 1000 GET requests to https://example.com with 10 parallel dodos (threads) and a timeout of 2 seconds: 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 -t 2s dodo -u https://example.com -m GET -d 10 -r 1000 -o 1m -t 2s
``` ```
With Docker: With Docker:
```sh ```sh
docker run --rm -i aykhans/dodo -u https://example.com -m GET -d 10 -r 1000 -t 2s docker run --rm -i aykhans/dodo -u https://example.com -m GET -d 10 -r 1000 -o 1m -t 2s
``` ```
### 2. Config File Usage ### 2. Config File Usage
#### 2.1 JSON Example 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:
Send 1000 GET requests to https://example.com with 10 parallel dodos (threads) and a timeout of 800 milliseconds: #### 2.1 JSON Example
```jsonc ```jsonc
{ {
@ -99,6 +83,8 @@ Send 1000 GET requests to https://example.com with 10 parallel dodos (threads) a
"timeout": "800ms", "timeout": "800ms",
"dodos": 10, "dodos": 10,
"requests": 1000, "requests": 1000,
"duration": "250s",
"skip_verify": false,
"params": [ "params": [
// A random value will be selected from the list for first "key1" param on each request // A random value will be selected from the list for first "key1" param on each request
@ -176,6 +162,8 @@ yes: false
timeout: "800ms" timeout: "800ms"
dodos: 10 dodos: 10
requests: 1000 requests: 1000
duration: "250s"
skip_verify: false
params: params:
# A random value will be selected from the list for first "key1" param on each request # A random value will be selected from the list for first "key1" param on each request
@ -247,30 +235,32 @@ docker run --rm -i aykhans/dodo -f https://example.com/config.yaml
CLI arguments override config file values: CLI arguments override config file values:
```sh ```sh
dodo -f /path/to/config.yaml -u https://example.com -m GET -d 10 -r 1000 -t 5s dodo -f /path/to/config.yaml -u https://example.com -m GET -d 10 -r 1000 -o 1m -t 5s
``` ```
With Docker: With Docker:
```sh ```sh
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 -t 5s 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
``` ```
## Config Parameters Reference ## Config Parameters Reference
If `Headers`, `Params`, `Cookies`, `Body`, or `Proxy` fields have multiple values, each request will choose a random value from the list. If `Headers`, `Params`, `Cookies`, `Body`, or `Proxy` fields have multiple values, each request will choose a random value from the list.
| Parameter | JSON config file | CLI Flag | CLI Short Flag | Type | Description | Default | | 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 | - | | 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 | | Yes | yes | -yes | -y | Boolean | Answer yes to all questions | false |
| URL | url | -url | -u | String | URL to send the request to | - | | URL | url | -url | -u | String | URL to send the request to | - |
| Method | method | -method | -m | String | HTTP method | GET | | Method | method | -method | -m | String | HTTP method | GET |
| Requests | requests | -requests | -r | UnsignedInteger | Total number of requests to send | 1000 |
| Dodos (Threads) | dodos | -dodos | -d | UnsignedInteger | Number of dodos (threads) to send requests in parallel | 1 | | Dodos (Threads) | dodos | -dodos | -d | UnsignedInteger | Number of dodos (threads) to send requests in parallel | 1 |
| Timeout | timeout | -timeout | -t | Duration | Timeout for canceling each request | 10s | | 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 | - | | Params | params | -param | -p | [{String: String OR [String]}] | Request parameters | - |
| Headers | headers | -header | -H | [{String: String OR [String]}] | Request headers | - | | Headers | headers | -header | -H | [{String: String OR [String]}] | Request headers | - |
| Cookies | cookies | -cookie | -c | [{String: String OR [String]}] | Request cookies | - | | Cookies | cookies | -cookie | -c | [{String: String OR [String]}] | Request cookies | - |
| Body | body | -body | -b | String OR [String] | Request body or list of request bodies | - | | 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 | - | | Proxy | proxies | -proxy | -x | String OR [String] | Proxy URL or list of proxy URLs | - |
| Skip Verify | skip_verify | -skip-verify | | Boolean | Skip SSL/TLS certificate verification | false |

53
Taskfile.yaml Normal file
View File

@ -0,0 +1,53 @@
# 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
ftl:
cmds:
- task: fmt
- task: tidy
- task: lint
fmt: gofmt -w -d .
tidy: go mod tidy
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"

View File

@ -1,32 +0,0 @@
#!/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

@ -5,6 +5,8 @@
"timeout": "5s", "timeout": "5s",
"dodos": 8, "dodos": 8,
"requests": 1000, "requests": 1000,
"duration": "10s",
"skip_verify": false,
"params": [ "params": [
{ "key1": ["value1", "value2", "value3", "value4"] }, { "key1": ["value1", "value2", "value3", "value4"] },

View File

@ -4,6 +4,8 @@ yes: false
timeout: "5s" timeout: "5s"
dodos: 8 dodos: 8
requests: 1000 requests: 1000
duration: "10s"
skip_verify: false
params: params:
- key1: ["value1", "value2", "value3", "value4"] - key1: ["value1", "value2", "value3", "value4"]

View File

@ -16,8 +16,8 @@ const cliUsageText = `Usage:
Examples: Examples:
Simple usage only with URL: Simple usage:
dodo -u https://example.com dodo -u https://example.com -o 1m
Usage with config file: Usage with config file:
dodo -f /path/to/config/file/config.json dodo -f /path/to/config/file/config.json
@ -25,13 +25,13 @@ Usage with config file:
Usage with all flags: Usage with all flags:
dodo -f /path/to/config/file/config.json \ dodo -f /path/to/config/file/config.json \
-u https://example.com -m POST \ -u https://example.com -m POST \
-d 10 -r 1000 -t 3s \ -d 10 -r 1000 -o 3m -t 3s \
-b "body1" -body "body2" \ -b "body1" -body "body2" \
-H "header1:value1" -header "header2:value2" \ -H "header1:value1" -header "header2:value2" \
-p "param1=value1" -param "param2=value2" \ -p "param1=value1" -param "param2=value2" \
-c "cookie1=value1" -cookie "cookie2=value2" \ -c "cookie1=value1" -cookie "cookie2=value2" \
-x "http://proxy.example.com:8080" -proxy "socks5://proxy2.example.com:8080" \ -x "http://proxy.example.com:8080" -proxy "socks5://proxy2.example.com:8080" \
-y -skip-verify -y
Flags: Flags:
-h, -help help for dodo -h, -help help for dodo
@ -39,15 +39,17 @@ Flags:
-y, -yes bool Answer yes to all questions (default %v) -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 -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) -d, -dodos uint Number of dodos(threads) (default %d)
-r, -requests uint Number of total requests (default %d) -r, -requests uint Number of total requests
-t, -timeout Duration Timeout for each request (e.g. 400ms, 15s, 1m10s) (default %v) -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 -u, -url string URL for stress testing
-m, -method string HTTP Method for the request (default %s) -m, -method string HTTP Method for the request (default %s)
-b, -body [string] Body for the request (e.g. "body text") -b, -body [string] Body for the request (e.g. "body text")
-p, -param [string] Parameter for the request (e.g. "key1=value1") -p, -param [string] Parameter for the request (e.g. "key1=value1")
-H, -header [string] Header 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") -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")` -x, -proxy [string] Proxy for the request (e.g. "http://proxy.example.com:8080")
-skip-verify bool Skip SSL/TLS certificate verification (default %v)`
func (config *Config) ReadCLI() (types.ConfigFile, error) { func (config *Config) ReadCLI() (types.ConfigFile, error) {
flag.Usage = func() { flag.Usage = func() {
@ -55,9 +57,9 @@ func (config *Config) ReadCLI() (types.ConfigFile, error) {
cliUsageText+"\n", cliUsageText+"\n",
DefaultYes, DefaultYes,
DefaultDodosCount, DefaultDodosCount,
DefaultRequestCount,
DefaultTimeout, DefaultTimeout,
DefaultMethod, DefaultMethod,
DefaultSkipVerify,
) )
} }
@ -65,11 +67,13 @@ func (config *Config) ReadCLI() (types.ConfigFile, error) {
version = false version = false
configFile = "" configFile = ""
yes = false yes = false
skipVerify = false
method = "" method = ""
url types.RequestURL url types.RequestURL
dodosCount = uint(0) dodosCount = uint(0)
requestCount = uint(0) requestCount = uint(0)
timeout time.Duration timeout time.Duration
duration time.Duration
) )
{ {
@ -82,6 +86,8 @@ func (config *Config) ReadCLI() (types.ConfigFile, error) {
flag.BoolVar(&yes, "yes", false, "Answer yes to all questions") flag.BoolVar(&yes, "yes", false, "Answer yes to all questions")
flag.BoolVar(&yes, "y", false, "Answer yes to all questions") flag.BoolVar(&yes, "y", false, "Answer yes to all questions")
flag.BoolVar(&skipVerify, "skip-verify", false, "Skip SSL/TLS certificate verification")
flag.StringVar(&method, "method", "", "HTTP Method") flag.StringVar(&method, "method", "", "HTTP Method")
flag.StringVar(&method, "m", "", "HTTP Method") flag.StringVar(&method, "m", "", "HTTP Method")
@ -94,6 +100,9 @@ func (config *Config) ReadCLI() (types.ConfigFile, error) {
flag.UintVar(&requestCount, "requests", 0, "Number of total requests") flag.UintVar(&requestCount, "requests", 0, "Number of total requests")
flag.UintVar(&requestCount, "r", 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, "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.DurationVar(&timeout, "t", 0, "Timeout for each request (e.g. 400ms, 15s, 1m10s)")
@ -139,10 +148,14 @@ func (config *Config) ReadCLI() (types.ConfigFile, error) {
config.DodosCount = utils.ToPtr(dodosCount) config.DodosCount = utils.ToPtr(dodosCount)
case "requests", "r": case "requests", "r":
config.RequestCount = utils.ToPtr(requestCount) config.RequestCount = utils.ToPtr(requestCount)
case "duration", "o":
config.Duration = &types.Duration{Duration: duration}
case "timeout", "t": case "timeout", "t":
config.Timeout = &types.Timeout{Duration: timeout} config.Timeout = &types.Timeout{Duration: timeout}
case "yes", "y": case "yes", "y":
config.Yes = utils.ToPtr(yes) config.Yes = utils.ToPtr(yes)
case "skip-verify":
config.SkipVerify = utils.ToPtr(skipVerify)
} }
}) })

View File

@ -15,13 +15,15 @@ import (
) )
const ( const (
VERSION string = "0.6.1" VERSION string = "0.6.5"
DefaultUserAgent string = "Dodo/" + VERSION DefaultUserAgent string = "Dodo/" + VERSION
DefaultMethod string = "GET" DefaultMethod string = "GET"
DefaultTimeout time.Duration = time.Second * 10 DefaultTimeout time.Duration = time.Second * 10
DefaultDodosCount uint = 1 DefaultDodosCount uint = 1
DefaultRequestCount uint = 1 DefaultRequestCount uint = 0
DefaultDuration time.Duration = 0
DefaultYes bool = false DefaultYes bool = false
DefaultSkipVerify bool = false
) )
var SupportedProxySchemes []string = []string{"http", "socks5", "socks5h"} var SupportedProxySchemes []string = []string{"http", "socks5", "socks5h"}
@ -32,7 +34,9 @@ type RequestConfig struct {
Timeout time.Duration Timeout time.Duration
DodosCount uint DodosCount uint
RequestCount uint RequestCount uint
Duration time.Duration
Yes bool Yes bool
SkipVerify bool
Params types.Params Params types.Params
Headers types.Headers Headers types.Headers
Cookies types.Cookies Cookies types.Cookies
@ -47,7 +51,9 @@ func NewRequestConfig(conf *Config) *RequestConfig {
Timeout: conf.Timeout.Duration, Timeout: conf.Timeout.Duration,
DodosCount: *conf.DodosCount, DodosCount: *conf.DodosCount,
RequestCount: *conf.RequestCount, RequestCount: *conf.RequestCount,
Duration: conf.Duration.Duration,
Yes: *conf.Yes, Yes: *conf.Yes,
SkipVerify: *conf.SkipVerify,
Params: conf.Params, Params: conf.Params,
Headers: conf.Headers, Headers: conf.Headers,
Cookies: conf.Cookies, Cookies: conf.Cookies,
@ -57,6 +63,9 @@ func NewRequestConfig(conf *Config) *RequestConfig {
} }
func (rc *RequestConfig) GetValidDodosCountForRequests() uint { func (rc *RequestConfig) GetValidDodosCountForRequests() uint {
if rc.RequestCount == 0 {
return rc.DodosCount
}
return min(rc.DodosCount, rc.RequestCount) return min(rc.DodosCount, rc.RequestCount)
} }
@ -95,7 +104,17 @@ func (rc *RequestConfig) Print() {
t.AppendSeparator() t.AppendSeparator()
t.AppendRow(table.Row{"Dodos", rc.DodosCount}) t.AppendRow(table.Row{"Dodos", rc.DodosCount})
t.AppendSeparator() t.AppendSeparator()
if rc.RequestCount > 0 {
t.AppendRow(table.Row{"Requests", rc.RequestCount}) t.AppendRow(table.Row{"Requests", rc.RequestCount})
} else {
t.AppendRow(table.Row{"Requests"})
}
t.AppendSeparator()
if rc.Duration > 0 {
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{"Params", rc.Params.String()})
t.AppendSeparator() t.AppendSeparator()
@ -106,6 +125,8 @@ func (rc *RequestConfig) Print() {
t.AppendRow(table.Row{"Proxy", rc.Proxies.String()}) t.AppendRow(table.Row{"Proxy", rc.Proxies.String()})
t.AppendSeparator() t.AppendSeparator()
t.AppendRow(table.Row{"Body", rc.Body.String()}) t.AppendRow(table.Row{"Body", rc.Body.String()})
t.AppendSeparator()
t.AppendRow(table.Row{"Skip Verify", rc.SkipVerify})
t.Render() t.Render()
} }
@ -116,7 +137,9 @@ type Config struct {
Timeout *types.Timeout `json:"timeout" yaml:"timeout"` Timeout *types.Timeout `json:"timeout" yaml:"timeout"`
DodosCount *uint `json:"dodos" yaml:"dodos"` DodosCount *uint `json:"dodos" yaml:"dodos"`
RequestCount *uint `json:"requests" yaml:"requests"` RequestCount *uint `json:"requests" yaml:"requests"`
Duration *types.Duration `json:"duration" yaml:"duration"`
Yes *bool `json:"yes" yaml:"yes"` Yes *bool `json:"yes" yaml:"yes"`
SkipVerify *bool `json:"skip_verify" yaml:"skip_verify"`
Params types.Params `json:"params" yaml:"params"` Params types.Params `json:"params" yaml:"params"`
Headers types.Headers `json:"headers" yaml:"headers"` Headers types.Headers `json:"headers" yaml:"headers"`
Cookies types.Cookies `json:"cookies" yaml:"cookies"` Cookies types.Cookies `json:"cookies" yaml:"cookies"`
@ -128,20 +151,20 @@ func NewConfig() *Config {
return &Config{} return &Config{}
} }
func (c *Config) Validate() []error { func (config *Config) Validate() []error {
var errs []error var errs []error
if utils.IsNilOrZero(c.URL) { if utils.IsNilOrZero(config.URL) {
errs = append(errs, errors.New("request URL is required")) errs = append(errs, errors.New("request URL is required"))
} else { } else {
if c.URL.Scheme == "" { if config.URL.Scheme == "" {
c.URL.Scheme = "http" config.URL.Scheme = "http"
} }
if c.URL.Scheme != "http" && c.URL.Scheme != "https" { if config.URL.Scheme != "http" && config.URL.Scheme != "https" {
errs = append(errs, errors.New("request URL scheme must be http or https")) errs = append(errs, errors.New("request URL scheme must be http or https"))
} }
urlParams := types.Params{} urlParams := types.Params{}
for key, values := range c.URL.Query() { for key, values := range config.URL.Query() {
for _, value := range values { for _, value := range values {
urlParams = append(urlParams, types.KeyValue[string, []string]{ urlParams = append(urlParams, types.KeyValue[string, []string]{
Key: key, Key: key,
@ -149,24 +172,24 @@ func (c *Config) Validate() []error {
}) })
} }
} }
c.Params = append(urlParams, c.Params...) config.Params = append(urlParams, config.Params...)
c.URL.RawQuery = "" config.URL.RawQuery = ""
} }
if utils.IsNilOrZero(c.Method) { if utils.IsNilOrZero(config.Method) {
errs = append(errs, errors.New("request method is required")) errs = append(errs, errors.New("request method is required"))
} }
if utils.IsNilOrZero(c.Timeout) { if utils.IsNilOrZero(config.Timeout) {
errs = append(errs, errors.New("request timeout must be greater than 0")) errs = append(errs, errors.New("request timeout must be greater than 0"))
} }
if utils.IsNilOrZero(c.DodosCount) { if utils.IsNilOrZero(config.DodosCount) {
errs = append(errs, errors.New("dodos count must be greater than 0")) errs = append(errs, errors.New("dodos count must be greater than 0"))
} }
if utils.IsNilOrZero(c.RequestCount) { if utils.IsNilOrZero(config.Duration) && utils.IsNilOrZero(config.RequestCount) {
errs = append(errs, errors.New("request count must be greater than 0")) errs = append(errs, errors.New("you should provide at least one of duration or request count"))
} }
for i, proxy := range c.Proxies { for i, proxy := range config.Proxies {
if proxy.String() == "" { if proxy.String() == "" {
errs = append(errs, fmt.Errorf("proxies[%d]: proxy cannot be empty", i)) errs = append(errs, fmt.Errorf("proxies[%d]: proxy cannot be empty", i))
} else if schema := proxy.Scheme; !slices.Contains(SupportedProxySchemes, schema) { } else if schema := proxy.Scheme; !slices.Contains(SupportedProxySchemes, schema) {
@ -197,9 +220,15 @@ func (config *Config) MergeConfig(newConfig *Config) {
if newConfig.RequestCount != nil { if newConfig.RequestCount != nil {
config.RequestCount = newConfig.RequestCount config.RequestCount = newConfig.RequestCount
} }
if newConfig.Duration != nil {
config.Duration = newConfig.Duration
}
if newConfig.Yes != nil { if newConfig.Yes != nil {
config.Yes = newConfig.Yes config.Yes = newConfig.Yes
} }
if newConfig.SkipVerify != nil {
config.SkipVerify = newConfig.SkipVerify
}
if len(newConfig.Params) != 0 { if len(newConfig.Params) != 0 {
config.Params = newConfig.Params config.Params = newConfig.Params
} }
@ -230,7 +259,14 @@ func (config *Config) SetDefaults() {
if config.RequestCount == nil { if config.RequestCount == nil {
config.RequestCount = utils.ToPtr(DefaultRequestCount) config.RequestCount = utils.ToPtr(DefaultRequestCount)
} }
if config.Duration == nil {
config.Duration = &types.Duration{Duration: DefaultDuration}
}
if config.Yes == nil { if config.Yes == nil {
config.Yes = utils.ToPtr(DefaultYes) config.Yes = utils.ToPtr(DefaultYes)
} }
if config.SkipVerify == nil {
config.SkipVerify = utils.ToPtr(DefaultSkipVerify)
}
config.Headers.SetIfNotExists("User-Agent", DefaultUserAgent)
} }

View File

@ -34,7 +34,7 @@ func (config *Config) ReadFile(filePath types.ConfigFile) error {
if err != nil { if err != nil {
return fmt.Errorf("failed to fetch config file from %s", filePath) return fmt.Errorf("failed to fetch config file from %s", filePath)
} }
defer resp.Body.Close() defer func() { _ = resp.Body.Close() }()
data, err = io.ReadAll(io.Reader(resp.Body)) data, err = io.ReadAll(io.Reader(resp.Body))
if err != nil { if err != nil {
@ -47,9 +47,10 @@ func (config *Config) ReadFile(filePath types.ConfigFile) error {
} }
} }
if fileExt == "json" { switch fileExt {
case "json":
return parseJSONConfig(data, config) return parseJSONConfig(data, config)
} else if fileExt == "yml" || fileExt == "yaml" { case "yml", "yaml":
return parseYAMLConfig(data, config) return parseYAMLConfig(data, config)
} }
} }

14
go.mod
View File

@ -1,21 +1,21 @@
module github.com/aykhans/dodo module github.com/aykhans/dodo
go 1.24.0 go 1.24.2
require ( require (
github.com/jedib0t/go-pretty/v6 v6.6.7 github.com/jedib0t/go-pretty/v6 v6.6.7
github.com/valyala/fasthttp v1.59.0 github.com/valyala/fasthttp v1.62.0
gopkg.in/yaml.v3 v3.0.1 gopkg.in/yaml.v3 v3.0.1
) )
require ( require (
github.com/andybalholm/brotli v1.1.1 // indirect github.com/andybalholm/brotli v1.1.1 // indirect
github.com/klauspost/compress v1.17.11 // indirect github.com/klauspost/compress v1.18.0 // indirect
github.com/mattn/go-runewidth v0.0.16 // indirect github.com/mattn/go-runewidth v0.0.16 // indirect
github.com/rivo/uniseg v0.4.7 // indirect github.com/rivo/uniseg v0.4.7 // indirect
github.com/valyala/bytebufferpool v1.0.0 // indirect github.com/valyala/bytebufferpool v1.0.0 // indirect
golang.org/x/net v0.36.0 // indirect golang.org/x/net v0.40.0 // indirect
golang.org/x/sys v0.30.0 // indirect golang.org/x/sys v0.33.0 // indirect
golang.org/x/term v0.29.0 // indirect golang.org/x/term v0.32.0 // indirect
golang.org/x/text v0.22.0 // indirect golang.org/x/text v0.25.0 // indirect
) )

24
go.sum
View File

@ -4,8 +4,8 @@ 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/jedib0t/go-pretty/v6 v6.6.7 h1:m+LbHpm0aIAPLzLbMfn8dc3Ht8MW7lsSO4MPItz/Uuo=
github.com/jedib0t/go-pretty/v6 v6.6.7/go.mod h1:YwC5CE4fJ1HFUDeivSV1r//AmANFHyqczZk+U6BDALU= github.com/jedib0t/go-pretty/v6 v6.6.7/go.mod h1:YwC5CE4fJ1HFUDeivSV1r//AmANFHyqczZk+U6BDALU=
github.com/klauspost/compress v1.17.11 h1:In6xLpyWOi1+C7tXUUWv2ot1QvBjxevKAaI6IXrJmUc= github.com/klauspost/compress v1.18.0 h1:c/Cqfb0r+Yi+JtIEq73FWXVkRonBlf0CRNYc8Zttxdo=
github.com/klauspost/compress v1.17.11/go.mod h1:pMDklpSncoRMuLFrf1W9Ss9KT+0rH90U12bZKk7uwG0= github.com/klauspost/compress v1.18.0/go.mod h1:2Pp+KzxcywXVXMr50+X0Q/Lsb43OQHYWRCY2AiWywWQ=
github.com/mattn/go-runewidth v0.0.16 h1:E5ScNMtiwvlvB5paMFdw9p4kSQzbXFikJ5SQO6TULQc= github.com/mattn/go-runewidth v0.0.16 h1:E5ScNMtiwvlvB5paMFdw9p4kSQzbXFikJ5SQO6TULQc=
github.com/mattn/go-runewidth v0.0.16/go.mod h1:Jdepj2loyihRzMpdS35Xk/zdY8IAYHsh153qUoGf23w= github.com/mattn/go-runewidth v0.0.16/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=
@ -17,18 +17,18 @@ github.com/stretchr/testify v1.10.0 h1:Xv5erBjTwe/5IxqUQTdXv5kgmIvbHo3QQyRwhJsOf
github.com/stretchr/testify v1.10.0/go.mod h1:r2ic/lqez/lEtzL7wO/rwa5dbSLXVDPFyf8C91i36aY= github.com/stretchr/testify v1.10.0/go.mod h1:r2ic/lqez/lEtzL7wO/rwa5dbSLXVDPFyf8C91i36aY=
github.com/valyala/bytebufferpool v1.0.0 h1:GqA5TC/0021Y/b9FG4Oi9Mr3q7XYx6KllzawFIhcdPw= github.com/valyala/bytebufferpool v1.0.0 h1:GqA5TC/0021Y/b9FG4Oi9Mr3q7XYx6KllzawFIhcdPw=
github.com/valyala/bytebufferpool v1.0.0/go.mod h1:6bBcMArwyJ5K/AmCkWv1jt77kVWyCJ6HpOuEn7z0Csc= github.com/valyala/bytebufferpool v1.0.0/go.mod h1:6bBcMArwyJ5K/AmCkWv1jt77kVWyCJ6HpOuEn7z0Csc=
github.com/valyala/fasthttp v1.59.0 h1:Qu0qYHfXvPk1mSLNqcFtEk6DpxgA26hy6bmydotDpRI= github.com/valyala/fasthttp v1.62.0 h1:8dKRBX/y2rCzyc6903Zu1+3qN0H/d2MsxPPmVNamiH0=
github.com/valyala/fasthttp v1.59.0/go.mod h1:GTxNb9Bc6r2a9D0TWNSPwDz78UxnTGBViY3xZNEqyYU= github.com/valyala/fasthttp v1.62.0/go.mod h1:FCINgr4GKdKqV8Q0xv8b+UxPV+H/O5nNFo3D+r54Htg=
github.com/xyproto/randomstring v1.0.5 h1:YtlWPoRdgMu3NZtP45drfy1GKoojuR7hmRcnhZqKjWU= github.com/xyproto/randomstring v1.0.5 h1:YtlWPoRdgMu3NZtP45drfy1GKoojuR7hmRcnhZqKjWU=
github.com/xyproto/randomstring v1.0.5/go.mod h1:rgmS5DeNXLivK7YprL0pY+lTuhNQW3iGxZ18UQApw/E= github.com/xyproto/randomstring v1.0.5/go.mod h1:rgmS5DeNXLivK7YprL0pY+lTuhNQW3iGxZ18UQApw/E=
golang.org/x/net v0.36.0 h1:vWF2fRbw4qslQsQzgFqZff+BItCvGFQqKzKIzx1rmoA= golang.org/x/net v0.40.0 h1:79Xs7wF06Gbdcg4kdCCIQArK11Z1hr5POQ6+fIYHNuY=
golang.org/x/net v0.36.0/go.mod h1:bFmbeoIPfrw4sMHNhb4J9f6+tPziuGjq7Jk/38fxi1I= golang.org/x/net v0.40.0/go.mod h1:y0hY0exeL2Pku80/zKK7tpntoX23cqL3Oa6njdgRtds=
golang.org/x/sys v0.30.0 h1:QjkSwP/36a20jFYWkSue1YwXzLmsV5Gfq7Eiy72C1uc= golang.org/x/sys v0.33.0 h1:q3i8TbbEz+JRD9ywIRlyRAQbM0qF7hu24q3teo2hbuw=
golang.org/x/sys v0.30.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA= golang.org/x/sys v0.33.0/go.mod h1:BJP2sWEmIv4KK5OTEluFJCKSidICx8ciO85XgH3Ak8k=
golang.org/x/term v0.29.0 h1:L6pJp37ocefwRRtYPKSWOWzOtWSxVajvz2ldH/xi3iU= golang.org/x/term v0.32.0 h1:DR4lr0TjUs3epypdhTOkMmuF5CDFJ/8pOnbzMZPQ7bg=
golang.org/x/term v0.29.0/go.mod h1:6bl4lRlvVuDgSf3179VpIxBF0o10JUpXWOnI7nErv7s= golang.org/x/term v0.32.0/go.mod h1:uZG1FhGx848Sqfsq4/DlJr3xGGsYMu/L5GW4abiaEPQ=
golang.org/x/text v0.22.0 h1:bofq7m3/HAFvbF51jz3Q9wLg3jkvSPuiZu/pD1XwgtM= golang.org/x/text v0.25.0 h1:qVyWApTSYLk/drJRO5mDlNYskwQznZmkpV2c8q9zls4=
golang.org/x/text v0.22.0/go.mod h1:YRoo4H8PVmsu+E3Ou7cqLVH8oXWIHVoX0jqUWALQhfY= golang.org/x/text v0.25.0/go.mod h1:WEdwpYrmk1qmdHvhkSTNPm3app7v4rsT8F2UD6+VHIA=
gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405 h1:yhCVgyC4o1eVCa2tZl7eS0r+SDo693bJlVdllGtEeKM= gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405 h1:yhCVgyC4o1eVCa2tZl7eS0r+SDo693bJlVdllGtEeKM=
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=

View File

@ -2,6 +2,7 @@ package requests
import ( import (
"context" "context"
"crypto/tls"
"errors" "errors"
"math/rand" "math/rand"
"net/url" "net/url"
@ -17,11 +18,12 @@ type ClientGeneratorFunc func() *fasthttp.HostClient
// getClients initializes and returns a slice of fasthttp.HostClient based on the provided parameters. // getClients initializes and returns a slice of fasthttp.HostClient based on the provided parameters.
// It can either return clients with proxies or a single client without proxies. // It can either return clients with proxies or a single client without proxies.
func getClients( func getClients(
ctx context.Context, _ context.Context,
timeout time.Duration, timeout time.Duration,
proxies []url.URL, proxies []url.URL,
maxConns uint, maxConns uint,
URL url.URL, URL url.URL,
skipVerify bool,
) []*fasthttp.HostClient { ) []*fasthttp.HostClient {
isTLS := URL.Scheme == "https" isTLS := URL.Scheme == "https"
@ -41,6 +43,9 @@ func getClients(
clients = append(clients, &fasthttp.HostClient{ clients = append(clients, &fasthttp.HostClient{
MaxConns: int(maxConns), MaxConns: int(maxConns),
IsTLS: isTLS, IsTLS: isTLS,
TLSConfig: &tls.Config{
InsecureSkipVerify: skipVerify,
},
Addr: addr, Addr: addr,
Dial: dialFunc, Dial: dialFunc,
MaxIdleConnDuration: timeout, MaxIdleConnDuration: timeout,
@ -56,6 +61,9 @@ func getClients(
client := &fasthttp.HostClient{ client := &fasthttp.HostClient{
MaxConns: int(maxConns), MaxConns: int(maxConns),
IsTLS: isTLS, IsTLS: isTLS,
TLSConfig: &tls.Config{
InsecureSkipVerify: skipVerify,
},
Addr: URL.Host, Addr: URL.Host,
MaxIdleConnDuration: timeout, MaxIdleConnDuration: timeout,
MaxConnDuration: timeout, MaxConnDuration: timeout,
@ -72,13 +80,19 @@ func getClients(
func getDialFunc(proxy *url.URL, timeout time.Duration) (fasthttp.DialFunc, error) { func getDialFunc(proxy *url.URL, timeout time.Duration) (fasthttp.DialFunc, error) {
var dialer fasthttp.DialFunc var dialer fasthttp.DialFunc
if proxy.Scheme == "socks5" || proxy.Scheme == "socks5h" { switch proxy.Scheme {
case "socks5", "socks5h":
dialer = fasthttpproxy.FasthttpSocksDialerDualStack(proxy.String()) dialer = fasthttpproxy.FasthttpSocksDialerDualStack(proxy.String())
} else if proxy.Scheme == "http" { case "http":
dialer = fasthttpproxy.FasthttpHTTPDialerDualStackTimeout(proxy.String(), timeout) dialer = fasthttpproxy.FasthttpHTTPDialerDualStackTimeout(proxy.String(), timeout)
} else { default:
return nil, errors.New("unsupported proxy scheme") return nil, errors.New("unsupported proxy scheme")
} }
if dialer == nil {
return nil, errors.New("internal error: proxy dialer is nil")
}
return dialer, nil return dialer, nil
} }

View File

@ -17,7 +17,7 @@ import (
func streamProgress( func streamProgress(
ctx context.Context, ctx context.Context,
wg *sync.WaitGroup, wg *sync.WaitGroup,
total int64, total uint,
message string, message string,
increase <-chan int64, increase <-chan int64,
) { ) {
@ -27,21 +27,26 @@ 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: total, Total: int64(total),
} }
pw.AppendTracker(&dodosTracker) pw.AppendTracker(&dodosTracker)
for { for {
select { select {
case <-ctx.Done(): case <-ctx.Done():
if ctx.Err() != context.Canceled { if err := ctx.Err(); err == context.Canceled || err == context.DeadlineExceeded {
dodosTracker.MarkAsDone()
} else {
dodosTracker.MarkAsErrored() dodosTracker.MarkAsErrored()
} }
time.Sleep(time.Millisecond * 300)
fmt.Printf("\r") fmt.Printf("\r")
time.Sleep(time.Millisecond * 500)
pw.Stop()
return return
case value := <-increase: case value := <-increase:

View File

@ -166,9 +166,6 @@ func setRequestHeaders(req *fasthttp.Request, headers []types.KeyValue[string, s
for _, header := range headers { for _, header := range headers {
req.Header.Add(header.Key, header.Value) req.Header.Add(header.Key, header.Value)
} }
if req.Header.UserAgent() == nil {
req.Header.SetUserAgent(config.DefaultUserAgent)
}
} }
// setRequestCookies adds the cookies of the given request with the provided key-value pairs. // setRequestCookies adds the cookies of the given request with the provided key-value pairs.

View File

@ -20,12 +20,19 @@ import (
// - 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.
func Run(ctx context.Context, requestConfig *config.RequestConfig) (Responses, error) { func Run(ctx context.Context, requestConfig *config.RequestConfig) (Responses, error) {
if requestConfig.Duration > 0 {
var cancel context.CancelFunc
ctx, cancel = context.WithTimeout(ctx, requestConfig.Duration)
defer cancel()
}
clients := getClients( clients := getClients(
ctx, ctx,
requestConfig.Timeout, requestConfig.Timeout,
requestConfig.Proxies, requestConfig.Proxies,
requestConfig.GetMaxConns(fasthttp.DefaultMaxConnsPerHost), requestConfig.GetMaxConns(fasthttp.DefaultMaxConnsPerHost),
requestConfig.URL, requestConfig.URL,
requestConfig.SkipVerify,
) )
if clients == nil { if clients == nil {
return nil, types.ErrInterrupt return nil, types.ErrInterrupt
@ -58,18 +65,29 @@ func releaseDodos(
wg sync.WaitGroup wg sync.WaitGroup
streamWG sync.WaitGroup streamWG sync.WaitGroup
requestCountPerDodo uint requestCountPerDodo uint
dodosCount uint = requestConfig.GetValidDodosCountForRequests() dodosCount = requestConfig.GetValidDodosCountForRequests()
dodosCountInt int = int(dodosCount)
responses = make([][]*Response, dodosCount) responses = make([][]*Response, dodosCount)
increase = make(chan int64, requestConfig.RequestCount) increase = make(chan int64, requestConfig.RequestCount)
) )
wg.Add(dodosCountInt) wg.Add(int(dodosCount))
streamWG.Add(1) streamWG.Add(1)
streamCtx, streamCtxCancel := context.WithCancel(context.Background()) streamCtx, streamCtxCancel := context.WithCancel(context.Background())
go streamProgress(streamCtx, &streamWG, int64(requestConfig.RequestCount), "Dodos Working🔥", increase) go streamProgress(streamCtx, &streamWG, requestConfig.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 = requestConfig.RequestCount - (i * requestConfig.RequestCount / dodosCount)
@ -78,7 +96,7 @@ func releaseDodos(
(i * requestConfig.RequestCount / dodosCount) (i * requestConfig.RequestCount / dodosCount)
} }
go sendRequest( go sendRequestByCount(
ctx, ctx,
newRequest(*requestConfig, clients, int64(i)), newRequest(*requestConfig, clients, int64(i)),
requestConfig.Timeout, requestConfig.Timeout,
@ -88,17 +106,19 @@ func releaseDodos(
&wg, &wg,
) )
} }
}
wg.Wait() wg.Wait()
streamCtxCancel() streamCtxCancel()
streamWG.Wait() streamWG.Wait()
return utils.Flatten(responses) return utils.Flatten(responses)
} }
// sendRequest sends a specified number of HTTP requests concurrently with a given timeout. // sendRequestByCount 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 sendRequest( func sendRequestByCount(
ctx context.Context, ctx context.Context,
request *Request, request *Request,
timeout time.Duration, timeout time.Duration,
@ -142,3 +162,50 @@ func sendRequest(
}() }()
} }
} }
// 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,
})
increase <- 1
}()
}
}

View File

@ -13,12 +13,12 @@ type Body []string
func (body Body) String() string { func (body Body) String() string {
var buffer bytes.Buffer var buffer bytes.Buffer
if len(body) == 0 { if len(body) == 0 {
return string(buffer.Bytes()) return buffer.String()
} }
if len(body) == 1 { if len(body) == 1 {
buffer.WriteString(body[0]) buffer.WriteString(body[0])
return string(buffer.Bytes()) return buffer.String()
} }
buffer.WriteString(text.FgBlue.Sprint("Random") + "[\n") buffer.WriteString(text.FgBlue.Sprint("Random") + "[\n")
@ -41,7 +41,7 @@ func (body Body) String() string {
} }
buffer.WriteString("\n]") buffer.WriteString("\n]")
return string(buffer.Bytes()) return buffer.String()
} }
func (body *Body) UnmarshalJSON(b []byte) error { func (body *Body) UnmarshalJSON(b []byte) error {
@ -66,7 +66,7 @@ func (body *Body) UnmarshalJSON(b []byte) error {
return nil return nil
} }
func (body *Body) UnmarshalYAML(unmarshal func(interface{}) error) error { func (body *Body) UnmarshalYAML(unmarshal func(any) error) error {
var data any var data any
if err := unmarshal(&data); err != nil { if err := unmarshal(&data); err != nil {
return err return err

View File

@ -14,7 +14,7 @@ type Cookies []KeyValue[string, []string]
func (cookies Cookies) String() string { func (cookies Cookies) String() string {
var buffer bytes.Buffer var buffer bytes.Buffer
if len(cookies) == 0 { if len(cookies) == 0 {
return string(buffer.Bytes()) return buffer.String()
} }
indent := " " indent := " "
@ -53,7 +53,7 @@ func (cookies Cookies) String() string {
buffer.WriteString(",\n" + text.FgGreen.Sprintf("+%d cookies", remainingPairs)) buffer.WriteString(",\n" + text.FgGreen.Sprintf("+%d cookies", remainingPairs))
} }
return string(buffer.Bytes()) return buffer.String()
} }
func (cookies *Cookies) AppendByKey(key, value string) { func (cookies *Cookies) AppendByKey(key, value string) {
@ -99,7 +99,7 @@ func (cookies *Cookies) UnmarshalJSON(b []byte) error {
return nil return nil
} }
func (cookies *Cookies) UnmarshalYAML(unmarshal func(interface{}) error) error { func (cookies *Cookies) UnmarshalYAML(unmarshal func(any) error) error {
var raw []map[string]any var raw []map[string]any
if err := unmarshal(&raw); err != nil { if err := unmarshal(&raw); err != nil {
return err return err

View File

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

View File

@ -14,7 +14,7 @@ type Headers []KeyValue[string, []string]
func (headers Headers) String() string { func (headers Headers) String() string {
var buffer bytes.Buffer var buffer bytes.Buffer
if len(headers) == 0 { if len(headers) == 0 {
return string(buffer.Bytes()) return buffer.String()
} }
indent := " " indent := " "
@ -53,7 +53,7 @@ func (headers Headers) String() string {
buffer.WriteString(",\n" + text.FgGreen.Sprintf("+%d headers", remainingPairs)) buffer.WriteString(",\n" + text.FgGreen.Sprintf("+%d headers", remainingPairs))
} }
return string(buffer.Bytes()) return buffer.String()
} }
func (headers *Headers) AppendByKey(key, value string) { func (headers *Headers) AppendByKey(key, value string) {
@ -73,6 +73,15 @@ func (headers Headers) GetValue(key string) *[]string {
return nil 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 { func (headers *Headers) UnmarshalJSON(b []byte) error {
var data []map[string]any var data []map[string]any
if err := json.Unmarshal(b, &data); err != nil { if err := json.Unmarshal(b, &data); err != nil {
@ -99,7 +108,7 @@ func (headers *Headers) UnmarshalJSON(b []byte) error {
return nil return nil
} }
func (headers *Headers) UnmarshalYAML(unmarshal func(interface{}) error) error { func (headers *Headers) UnmarshalYAML(unmarshal func(any) error) error {
var raw []map[string]any var raw []map[string]any
if err := unmarshal(&raw); err != nil { if err := unmarshal(&raw); err != nil {
return err return err
@ -137,3 +146,11 @@ func (headers *Headers) Set(value string) error {
return nil 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

@ -14,7 +14,7 @@ type Params []KeyValue[string, []string]
func (params Params) String() string { func (params Params) String() string {
var buffer bytes.Buffer var buffer bytes.Buffer
if len(params) == 0 { if len(params) == 0 {
return string(buffer.Bytes()) return buffer.String()
} }
indent := " " indent := " "
@ -53,7 +53,7 @@ func (params Params) String() string {
buffer.WriteString(",\n" + text.FgGreen.Sprintf("+%d params", remainingPairs)) buffer.WriteString(",\n" + text.FgGreen.Sprintf("+%d params", remainingPairs))
} }
return string(buffer.Bytes()) return buffer.String()
} }
func (params *Params) AppendByKey(key, value string) { func (params *Params) AppendByKey(key, value string) {
@ -99,7 +99,7 @@ func (params *Params) UnmarshalJSON(b []byte) error {
return nil return nil
} }
func (params *Params) UnmarshalYAML(unmarshal func(interface{}) error) error { func (params *Params) UnmarshalYAML(unmarshal func(any) error) error {
var raw []map[string]any var raw []map[string]any
if err := unmarshal(&raw); err != nil { if err := unmarshal(&raw); err != nil {
return err return err

View File

@ -14,12 +14,12 @@ type Proxies []url.URL
func (proxies Proxies) String() string { func (proxies Proxies) String() string {
var buffer bytes.Buffer var buffer bytes.Buffer
if len(proxies) == 0 { if len(proxies) == 0 {
return string(buffer.Bytes()) return buffer.String()
} }
if len(proxies) == 1 { if len(proxies) == 1 {
buffer.WriteString(proxies[0].String()) buffer.WriteString(proxies[0].String())
return string(buffer.Bytes()) return buffer.String()
} }
buffer.WriteString(text.FgBlue.Sprint("Random") + "[\n") buffer.WriteString(text.FgBlue.Sprint("Random") + "[\n")
@ -42,7 +42,7 @@ func (proxies Proxies) String() string {
} }
buffer.WriteString("\n]") buffer.WriteString("\n]")
return string(buffer.Bytes()) return buffer.String()
} }
func (proxies *Proxies) UnmarshalJSON(b []byte) error { func (proxies *Proxies) UnmarshalJSON(b []byte) error {
@ -75,7 +75,7 @@ func (proxies *Proxies) UnmarshalJSON(b []byte) error {
return nil return nil
} }
func (proxies *Proxies) UnmarshalYAML(unmarshal func(interface{}) error) error { func (proxies *Proxies) UnmarshalYAML(unmarshal func(any) error) error {
var data any var data any
if err := unmarshal(&data); err != nil { if err := unmarshal(&data); err != nil {
return err return err

View File

@ -18,14 +18,14 @@ func (requestURL *RequestURL) UnmarshalJSON(data []byte) error {
parsedURL, err := url.Parse(urlStr) parsedURL, err := url.Parse(urlStr)
if err != nil { if err != nil {
return errors.New("Request URL is invalid") return errors.New("request URL is invalid")
} }
requestURL.URL = *parsedURL requestURL.URL = *parsedURL
return nil return nil
} }
func (requestURL *RequestURL) UnmarshalYAML(unmarshal func(interface{}) error) error { func (requestURL *RequestURL) UnmarshalYAML(unmarshal func(any) error) error {
var urlStr string var urlStr string
if err := unmarshal(&urlStr); err != nil { if err := unmarshal(&urlStr); err != nil {
return err return err
@ -33,7 +33,7 @@ func (requestURL *RequestURL) UnmarshalYAML(unmarshal func(interface{}) error) e
parsedURL, err := url.Parse(urlStr) parsedURL, err := url.Parse(urlStr)
if err != nil { if err != nil {
return errors.New("Request URL is invalid") return errors.New("request URL is invalid")
} }
requestURL.URL = *parsedURL requestURL.URL = *parsedURL

57
types/timeout.go Normal file
View File

@ -0,0 +1,57 @@
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(any) 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

@ -6,9 +6,5 @@ func IsNilOrZero[T comparable](value *T) bool {
} }
var zero T var zero T
if *value == zero { return *value == zero
return true
}
return false
} }

View File

@ -20,9 +20,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 int = len(values) clientsCount = len(values)
currentIndex int = localRand.Intn(clientsCount) currentIndex = localRand.Intn(clientsCount)
stopIndex int = currentIndex stopIndex = currentIndex
) )
return func() Value { return func() Value {