54 Commits

Author SHA1 Message Date
4a7db48351 🔨 Remove default 'http' schema from request URL 2025-06-28 23:44:04 +04:00
0adde6e04e Merge pull request #118 from aykhans/core/refactor
General Refactoring
2025-06-08 23:39:02 +04:00
ca50de4e2f ⬆️ bump version to 0.7.1 2025-06-08 20:57:01 +04:00
c99e7c66d9 📚 Update docs 2025-06-08 20:56:00 +04:00
280e5f5c4e 📚 Add EXAMPLES.md 2025-06-08 20:55:45 +04:00
47dfad6046 🔧 Add more template functions 2025-06-08 20:55:02 +04:00
5bb644d55f Merge pull request #116 from aykhans/feat/value-generator
Add value generators
2025-06-01 23:53:11 +04:00
9152eefdc5 Add 'body_FormData' generator to template func maps 2025-06-01 23:22:55 +04:00
a8cd253c63 Add string functions to templates func map 2025-06-01 20:52:27 +04:00
9aaf2db74d 📚 Update docs 2025-06-01 20:52:06 +04:00
5c3e254e1e 📚 Update docs 2025-05-30 21:51:11 +04:00
e5c681a22b ⬆️ bump version to 0.7.0 2025-05-30 21:44:10 +04:00
79668e4ece Add value generators 2025-05-30 21:41:38 +04:00
f248c2af96 Value generator initial commit 2025-05-30 10:40:20 +04:00
924bd819ee Merge pull request #115 from aykhans/core/refactor
🔧 Refactor 'RandomValueCycle' and 'getKeyValueGeneratorFunc' functions
2025-05-29 22:16:01 +04:00
e567155eb1 🔧 Refactor 'RandomValueCycle' and 'getKeyValueGeneratorFunc' functions 2025-05-29 22:10:41 +04:00
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
30 changed files with 2265 additions and 372 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

@ -6,7 +6,7 @@ COPY go.mod go.sum ./
RUN go mod download RUN go mod download
COPY . . COPY . .
RUN go build -ldflags "-s -w" -o dodo RUN CGO_ENABLED=0 go build -ldflags "-s -w" -o dodo
FROM gcr.io/distroless/static-debian12:latest FROM gcr.io/distroless/static-debian12:latest

934
EXAMPLES.md Normal file
View File

@ -0,0 +1,934 @@
# Dodo Usage Examples
This document provides comprehensive examples of using Dodo with various configuration combinations. Each example includes three methods: CLI usage, YAML configuration, and JSON configuration.
## Table of Contents
1. [Basic HTTP Stress Testing](#1-basic-http-stress-testing)
2. [POST Request with Form Data](#2-post-request-with-form-data)
3. [API Testing with Authentication](#3-api-testing-with-authentication)
4. [Testing with Custom Headers and Cookies](#4-testing-with-custom-headers-and-cookies)
5. [Load Testing with Proxy Rotation](#5-load-testing-with-proxy-rotation)
6. [JSON API Testing with Dynamic Data](#6-json-api-testing-with-dynamic-data)
7. [File Upload Testing](#7-file-upload-testing)
8. [E-commerce Cart Testing](#8-e-commerce-cart-testing)
9. [GraphQL API Testing](#9-graphql-api-testing)
10. [WebSocket-style HTTP Testing](#10-websocket-style-http-testing)
11. [Multi-tenant Application Testing](#11-multi-tenant-application-testing)
12. [Rate Limiting Testing](#12-rate-limiting-testing)
---
## 1. Basic HTTP Stress Testing
Test a simple website with basic GET requests to measure performance under load.
### CLI Usage
```bash
dodo -u https://httpbin.org/get \
-m GET \
-d 5 \
-r 100 \
-t 5s \
-o 30s \
--skip-verify=false \
-y
```
### YAML Configuration
```yaml
method: "GET"
url: "https://httpbin.org/get"
yes: true
timeout: "5s"
dodos: 5
requests: 100
duration: "30s"
skip_verify: false
```
### JSON Configuration
```json
{
"method": "GET",
"url": "https://httpbin.org/get",
"yes": true,
"timeout": "5s",
"dodos": 5,
"requests": 100,
"duration": "30s",
"skip_verify": false
}
```
---
## 2. POST Request with Form Data
Test form submission endpoints with randomized form data.
### CLI Usage
```bash
dodo -u https://httpbin.org/post \
-m POST \
-d 3 \
-r 50 \
-t 10s \
--skip-verify=false \
-H "Content-Type:application/x-www-form-urlencoded" \
-b "username={{ fakeit_Username }}&password={{ fakeit_Password true true true true true 12 }}&email={{ fakeit_Email }}" \
-b "username={{ fakeit_Username }}&password={{ fakeit_Password true true true true true 8 }}&email={{ fakeit_Email }}" \
-y
```
### YAML Configuration
```yaml
method: "POST"
url: "https://httpbin.org/post"
yes: true
timeout: "10s"
dodos: 3
requests: 50
skip_verify: false
headers:
- Content-Type: "application/x-www-form-urlencoded"
body:
- "username={{ fakeit_Username }}&password={{ fakeit_Password true true true true true 12 }}&email={{ fakeit_Email }}"
- "username={{ fakeit_Username }}&password={{ fakeit_Password true true true true true 8 }}&email={{ fakeit_Email }}"
```
### JSON Configuration
```json
{
"method": "POST",
"url": "https://httpbin.org/post",
"yes": true,
"timeout": "10s",
"dodos": 3,
"requests": 50,
"skip_verify": false,
"headers": [{ "Content-Type": "application/x-www-form-urlencoded" }],
"body": [
"username={{ fakeit_Username }}&password={{ fakeit_Password true true true true true 12 }}&email={{ fakeit_Email }}",
"username={{ fakeit_Username }}&password={{ fakeit_Password true true true true true 8 }}&email={{ fakeit_Email }}"
]
}
```
---
## 3. API Testing with Authentication
Test protected API endpoints with various authentication methods.
### CLI Usage
```bash
dodo -u https://httpbin.org/bearer \
-m GET \
-d 4 \
-r 200 \
-t 8s \
--skip-verify=false \
-H "Authorization:Bearer {{ fakeit_LetterN 32 }}" \
-H "User-Agent:{{ fakeit_UserAgent }}" \
-H "X-Request-ID:{{ fakeit_Int }}" \
-H "Accept:application/json" \
-p "api_version=v1" \
-p "format=json" \
-p "client_id=mobile" -p "client_id=web" -p "client_id=desktop" \
-y
```
### YAML Configuration
```yaml
method: "GET"
url: "https://httpbin.org/bearer"
yes: true
timeout: "8s"
dodos: 4
requests: 200
skip_verify: false
params:
- api_version: "v1"
- format: "json"
- client_id: ["mobile", "web", "desktop"]
headers:
- Authorization: "Bearer {{ fakeit_LetterN 32 }}"
- User-Agent: "{{ fakeit_UserAgent }}"
- X-Request-ID: "{{ fakeit_Int }}"
- Accept: "application/json"
```
### JSON Configuration
```json
{
"method": "GET",
"url": "https://httpbin.org/bearer",
"yes": true,
"timeout": "8s",
"dodos": 4,
"requests": 200,
"skip_verify": false,
"params": [
{ "api_version": "v1" },
{ "format": "json" },
{ "client_id": ["mobile", "web", "desktop"] }
],
"headers": [
{ "Authorization": "Bearer {{ fakeit_LetterN 32 }}" },
{ "User-Agent": "{{ fakeit_UserAgent }}" },
{ "X-Request-ID": "{{ fakeit_Int }}" },
{ "Accept": "application/json" }
]
}
```
---
## 4. Testing with Custom Headers and Cookies
Test applications that require specific headers and session cookies.
### CLI Usage
```bash
dodo -u https://httpbin.org/cookies \
-m GET \
-d 6 \
-r 75 \
-t 5s \
--skip-verify=false \
-H 'Accept-Language:{{ strings_Join "," (fakeit_LanguageAbbreviation) (fakeit_LanguageAbbreviation) (fakeit_LanguageAbbreviation) }}' \
-H "X-Forwarded-For:{{ fakeit_IPv4Address }}" \
-H "X-Real-IP:{{ fakeit_IPv4Address }}" \
-H "Accept-Encoding:gzip" -H "Accept-Encoding:deflate" -H "Accept-Encoding:br" \
-c "session_id={{ fakeit_UUID }}" \
-c 'user_pref={{ fakeit_RandomString "a1" "b2" "c3" }}' \
-c "theme=dark" -c "theme=light" -c "theme=auto" \
-c "lang=en" -c "lang=es" -c "lang=fr" -c "lang=de" \
-y
```
### YAML Configuration
```yaml
method: "GET"
url: "https://httpbin.org/cookies"
yes: true
timeout: "5s"
dodos: 6
requests: 75
skip_verify: false
headers:
- Accept-Language: '{{ strings_Join "," (fakeit_LanguageAbbreviation) (fakeit_LanguageAbbreviation) (fakeit_LanguageAbbreviation) }}'
- X-Forwarded-For: "{{ fakeit_IPv4Address }}"
- X-Real-IP: "{{ fakeit_IPv4Address }}"
- Accept-Encoding: ["gzip", "deflate", "br"]
cookies:
- session_id: "{{ fakeit_UUID }}"
- user_pref: '{{ fakeit_RandomString "a1" "b2" "c3" }}'
- theme: ["dark", "light", "auto"]
- lang: ["en", "es", "fr", "de"]
```
### JSON Configuration
```json
{
"method": "GET",
"url": "https://httpbin.org/cookies",
"yes": true,
"timeout": "5s",
"dodos": 6,
"requests": 75,
"skip_verify": false,
"headers": [
{
"Accept-Language": "{{ strings_Join \",\" (fakeit_LanguageAbbreviation) (fakeit_LanguageAbbreviation) (fakeit_LanguageAbbreviation) }}"
},
{ "X-Forwarded-For": "{{ fakeit_IPv4Address }}" },
{ "X-Real-IP": "{{ fakeit_IPv4Address }}" },
{ "Accept-Encoding": ["gzip", "deflate", "br"] }
],
"cookies": [
{ "session_id": "{{ fakeit_UUID }}" },
{ "user_pref": "{{ fakeit_RandomString \"a1\" \"b2\" \"c3\" }}" },
{ "theme": ["dark", "light", "auto"] },
{ "lang": ["en", "es", "fr", "de"] }
]
}
```
---
## 5. Load Testing with Proxy Rotation
Test through multiple proxies for distributed load testing.
### CLI Usage
```bash
dodo -u https://httpbin.org/ip \
-m GET \
-d 8 \
-r 300 \
-t 15s \
--skip-verify=false \
-x "http://proxy1.example.com:8080" \
-x "http://proxy2.example.com:8080" \
-x "socks5://proxy3.example.com:1080" \
-x "http://username:password@proxy4.example.com:8080" \
-H "User-Agent:{{ fakeit_UserAgent }}" \
-H "Accept:application/json" \
-y
```
### YAML Configuration
```yaml
method: "GET"
url: "https://httpbin.org/ip"
yes: true
timeout: "15s"
dodos: 8
requests: 300
skip_verify: false
proxy:
- "http://proxy1.example.com:8080"
- "http://proxy2.example.com:8080"
- "socks5://proxy3.example.com:1080"
- "http://username:password@proxy4.example.com:8080"
headers:
- User-Agent: "{{ fakeit_UserAgent }}"
- Accept: "application/json"
```
### JSON Configuration
```json
{
"method": "GET",
"url": "https://httpbin.org/ip",
"yes": true,
"timeout": "15s",
"dodos": 8,
"requests": 300,
"skip_verify": false,
"proxy": [
"http://proxy1.example.com:8080",
"http://proxy2.example.com:8080",
"socks5://proxy3.example.com:1080",
"http://username:password@proxy4.example.com:8080"
],
"headers": [
{ "User-Agent": "{{ fakeit_UserAgent }}" },
{ "Accept": "application/json" }
]
}
```
---
## 6. JSON API Testing with Dynamic Data
Test REST APIs with realistic JSON payloads.
### CLI Usage
```bash
dodo -u https://httpbin.org/post \
-m POST \
-d 5 \
-r 150 \
-t 12s \
--skip-verify=false \
-H "Content-Type:application/json" \
-H "Accept:application/json" \
-H "X-API-Version:2023-10-01" \
-b '{"user_id":{{ fakeit_Uint }},"name":"{{ fakeit_Name }}","email":"{{ fakeit_Email }}","created_at":"{{ fakeit_Date }}"}' \
-b '{"product_id":{{ fakeit_Uint }},"name":"{{ fakeit_ProductName }}","price":{{ fakeit_Price 10 1000 }},"category":"{{ fakeit_ProductCategory }}"}' \
-b '{"order_id":"{{ fakeit_UUID }}","items":[{"id":{{ fakeit_Uint }},"quantity":{{ fakeit_IntRange 1 10 }}}],"total":{{ fakeit_Price 50 500 }}}' \
-y
```
### YAML Configuration
```yaml
method: "POST"
url: "https://httpbin.org/post"
yes: true
timeout: "12s"
dodos: 5
requests: 150
skip_verify: false
headers:
- Content-Type: "application/json"
- Accept: "application/json"
- X-API-Version: "2023-10-01"
body:
- '{"user_id":{{ fakeit_Uint }},"name":"{{ fakeit_Name }}","email":"{{ fakeit_Email }}","created_at":"{{ fakeit_Date }}"}'
- '{"product_id":{{ fakeit_Uint }},"name":"{{ fakeit_ProductName }}","price":{{ fakeit_Price 10 1000 }},"category":"{{ fakeit_ProductCategory }}"}'
- '{"order_id":"{{ fakeit_UUID }}","items":[{"id":{{ fakeit_Uint }},"quantity":{{ fakeit_IntRange 1 10 }}}],"total":{{ fakeit_Price 50 500 }}}'
```
### JSON Configuration
```json
{
"method": "POST",
"url": "https://httpbin.org/post",
"yes": true,
"timeout": "12s",
"dodos": 5,
"requests": 150,
"skip_verify": false,
"headers": [
{ "Content-Type": "application/json" },
{ "Accept": "application/json" },
{ "X-API-Version": "2023-10-01" }
],
"body": [
"{\"user_id\":{{ fakeit_Uint }},\"name\":\"{{ fakeit_Name }}\",\"email\":\"{{ fakeit_Email }}\",\"created_at\":\"{{ fakeit_Date }}\"}",
"{\"product_id\":{{ fakeit_Uint }},\"name\":\"{{ fakeit_ProductName }}\",\"price\":{{ fakeit_Price 10 1000 }},\"category\":\"{{ fakeit_ProductCategory }}\"}",
"{\"order_id\":\"{{ fakeit_UUID }}\",\"items\":[{\"id\":{{ fakeit_Uint }},\"quantity\":{{ fakeit_IntRange 1 10 }}}],\"total\":{{ fakeit_Price 50 500 }}}"
]
}
```
---
## 7. File Upload Testing
Test file upload endpoints with multipart form data.
### CLI Usage
```bash
dodo -u https://httpbin.org/post \
-m POST \
-d 3 \
-r 25 \
-t 30s \
--skip-verify=false \
-H "X-Upload-Source:dodo-test" \
-H "User-Agent:{{ fakeit_UserAgent }}" \
-b '{{ body_FormData (dict_Str "filename" (fakeit_UUID) "content" (fakeit_Paragraph 3 5 10 " ")) }}' \
-b '{{ body_FormData (dict_Str "file" (fakeit_UUID) "description" (fakeit_Sentence 10) "category" "image") }}' \
-y
```
### YAML Configuration
```yaml
method: "POST"
url: "https://httpbin.org/post"
yes: true
timeout: "30s"
dodos: 3
requests: 25
skip_verify: false
headers:
- X-Upload-Source: "dodo-test"
- User-Agent: "{{ fakeit_UserAgent }}"
body:
- '{{ body_FormData (dict_Str "filename" (fakeit_UUID) "content" (fakeit_Paragraph 3 5 10 " ")) }}'
- '{{ body_FormData (dict_Str "file" (fakeit_UUID) "description" (fakeit_Sentence 10) "category" "image") }}'
```
### JSON Configuration
```json
{
"method": "POST",
"url": "https://httpbin.org/post",
"yes": true,
"timeout": "30s",
"dodos": 3,
"requests": 25,
"skip_verify": false,
"headers": [
{ "X-Upload-Source": "dodo-test" },
{ "User-Agent": "{{ fakeit_UserAgent }}" }
],
"body": [
"{{ body_FormData (dict_Str \"filename\" (fakeit_UUID) \"content\" (fakeit_Paragraph 3 5 10 \" \")) }}",
"{{ body_FormData (dict_Str \"file\" (fakeit_UUID) \"description\" (fakeit_Sentence 10) \"category\" \"image\") }}"
]
}
```
---
## 8. E-commerce Cart Testing
Test shopping cart operations with realistic product data.
### CLI Usage
```bash
dodo -u https://api.example-shop.com/cart \
-m POST \
-d 10 \
-r 500 \
-t 8s \
--skip-verify=false \
-H "Content-Type:application/json" \
-H "Authorization:Bearer {{ fakeit_LetterN 32 }}" \
-H "X-Client-Version:1.2.3" \
-H "User-Agent:{{ fakeit_UserAgent }}" \
-c "cart_session={{ fakeit_UUID }}" \
-c "user_pref=guest" -c "user_pref=member" -c "user_pref=premium" \
-c "region=US" -c "region=EU" -c "region=ASIA" \
-p "currency=USD" -p "currency=EUR" -p "currency=GBP" \
-p "locale=en-US" -p "locale=en-GB" -p "locale=de-DE" -p "locale=fr-FR" \
-b '{"action":"add","product_id":"{{ fakeit_UUID }}","quantity":{{ fakeit_IntRange 1 5 }},"user_id":"{{ fakeit_UUID }}"}' \
-b '{"action":"remove","product_id":"{{ fakeit_UUID }}","user_id":"{{ fakeit_UUID }}"}' \
-b '{"action":"update","product_id":"{{ fakeit_UUID }}","quantity":{{ fakeit_IntRange 1 10 }},"user_id":"{{ fakeit_UUID }}"}' \
-y
```
### YAML Configuration
```yaml
method: "POST"
url: "https://api.example-shop.com/cart"
yes: true
timeout: "8s"
dodos: 10
requests: 500
skip_verify: false
headers:
- Content-Type: "application/json"
- Authorization: "Bearer {{ fakeit_LetterN 32 }}"
- X-Client-Version: "1.2.3"
- User-Agent: "{{ fakeit_UserAgent }}"
cookies:
- cart_session: "{{ fakeit_UUID }}"
- user_pref: ["guest", "member", "premium"]
- region: ["US", "EU", "ASIA"]
params:
- currency: ["USD", "EUR", "GBP"]
- locale: ["en-US", "en-GB", "de-DE", "fr-FR"]
body:
- '{"action":"add","product_id":"{{ fakeit_UUID }}","quantity":{{ fakeit_IntRange 1 5 }},"user_id":"{{ fakeit_UUID }}"}'
- '{"action":"remove","product_id":"{{ fakeit_UUID }}","user_id":"{{ fakeit_UUID }}"}'
- '{"action":"update","product_id":"{{ fakeit_UUID }}","quantity":{{ fakeit_IntRange 1 10 }},"user_id":"{{ fakeit_UUID }}"}'
```
### JSON Configuration
```json
{
"method": "POST",
"url": "https://api.example-shop.com/cart",
"yes": true,
"timeout": "8s",
"dodos": 10,
"requests": 500,
"skip_verify": false,
"headers": [
{ "Content-Type": "application/json" },
{ "Authorization": "Bearer {{ fakeit_LetterN 32 }}" },
{ "X-Client-Version": "1.2.3" },
{ "User-Agent": "{{ fakeit_UserAgent }}" }
],
"cookies": [
{ "cart_session": "{{ fakeit_UUID }}" },
{ "user_pref": ["guest", "member", "premium"] },
{ "region": ["US", "EU", "ASIA"] }
],
"params": [
{ "currency": ["USD", "EUR", "GBP"] },
{ "locale": ["en-US", "en-GB", "de-DE", "fr-FR"] }
],
"body": [
"{\"action\":\"add\",\"product_id\":\"{{ fakeit_UUID }}\",\"quantity\":{{ fakeit_IntRange 1 5 }},\"user_id\":\"{{ fakeit_UUID }}\"}",
"{\"action\":\"remove\",\"product_id\":\"{{ fakeit_UUID }}\",\"user_id\":\"{{ fakeit_UUID }}\"}",
"{\"action\":\"update\",\"product_id\":\"{{ fakeit_UUID }}\",\"quantity\":{{ fakeit_IntRange 1 10 }},\"user_id\":\"{{ fakeit_UUID }}\"}"
]
}
```
---
## 9. GraphQL API Testing
Test GraphQL endpoints with various queries and mutations.
### CLI Usage
```bash
dodo -u https://api.example.com/graphql \
-m POST \
-d 4 \
-r 100 \
-t 10s \
--skip-verify=false \
-H "Content-Type:application/json" \
-H "Authorization:Bearer {{ fakeit_UUID }}" \
-H "X-GraphQL-Client:dodo-test" \
-b '{"query":"query GetUser($id: ID!) { user(id: $id) { id name email } }","variables":{"id":"{{ fakeit_UUID }}"}}' \
-b '{"query":"query GetPosts($limit: Int) { posts(limit: $limit) { id title content } }","variables":{"limit":{{ fakeit_IntRange 5 20 }}}}' \
-b '{"query":"mutation CreatePost($input: PostInput!) { createPost(input: $input) { id title } }","variables":{"input":{"title":"{{ fakeit_Sentence 5 }}","content":"{{ fakeit_Paragraph 2 3 5 " "}}","authorId":"{{ fakeit_UUID }}"}}}' \
-y
```
### YAML Configuration
```yaml
method: "POST"
url: "https://api.example.com/graphql"
yes: true
timeout: "10s"
dodos: 4
requests: 100
skip_verify: false
headers:
- Content-Type: "application/json"
- Authorization: "Bearer {{ fakeit_UUID }}"
- X-GraphQL-Client: "dodo-test"
body:
- '{"query":"query GetUser($id: ID!) { user(id: $id) { id name email } }","variables":{"id":"{{ fakeit_UUID }}"}}'
- '{"query":"query GetPosts($limit: Int) { posts(limit: $limit) { id title content } }","variables":{"limit":{{ fakeit_IntRange 5 20 }}}}'
- '{"query":"mutation CreatePost($input: PostInput!) { createPost(input: $input) { id title } }","variables":{"input":{"title":"{{ fakeit_Sentence 5 }}","content":"{{ fakeit_Paragraph 2 3 5 " "}}","authorId":"{{ fakeit_UUID }}"}}}'
```
### JSON Configuration
```json
{
"method": "POST",
"url": "https://api.example.com/graphql",
"yes": true,
"timeout": "10s",
"dodos": 4,
"requests": 100,
"skip_verify": false,
"headers": [
{ "Content-Type": "application/json" },
{ "Authorization": "Bearer {{ fakeit_UUID }}" },
{ "X-GraphQL-Client": "dodo-test" }
],
"body": [
"{\"query\":\"query GetUser($id: ID!) { user(id: $id) { id name email } }\",\"variables\":{\"id\":\"{{ fakeit_UUID }}\"}}",
"{\"query\":\"query GetPosts($limit: Int) { posts(limit: $limit) { id title content } }\",\"variables\":{\"limit\":{{ fakeit_IntRange 5 20 }}}}",
"{\"query\":\"mutation CreatePost($input: PostInput!) { createPost(input: $input) { id title } }\",\"variables\":{\"input\":{\"title\":\"{{ fakeit_Sentence 5 }}\",\"content\":\"{{ fakeit_Paragraph 2 3 5 \\\" \\\"}}\",\"authorId\":\"{{ fakeit_UUID }}\"}}}"
]
}
```
---
## 10. WebSocket-style HTTP Testing
Test real-time applications with WebSocket-like HTTP endpoints.
### CLI Usage
```bash
dodo -u https://api.realtime-app.com/events \
-m POST \
-d 15 \
-r 1000 \
-t 5s \
-o 60s \
--skip-verify=false \
-H "Content-Type:application/json" \
-H "X-Event-Type:{{ fakeit_LetterNN 4 12 }}" \
-H "Connection:keep-alive" \
-H "Cache-Control:no-cache" \
-c "connection_id={{ fakeit_UUID }}" \
-c "session_token={{ fakeit_UUID }}" \
-p "channel=general" -p "channel=notifications" -p "channel=alerts" -p "channel=updates" \
-p "version=v1" -p "version=v2" \
-b '{"event":"{{ fakeit_Word }}","data":{"timestamp":"{{ fakeit_Date }}","user_id":"{{ fakeit_UUID }}","message":"{{ fakeit_Sentence 8 }}"}}' \
-b '{"event":"ping","data":{"timestamp":"{{ fakeit_Date }}","client_id":"{{ fakeit_UUID }}"}}' \
-b '{"event":"status_update","data":{"status":"{{ fakeit_Word }}","user_id":"{{ fakeit_UUID }}","timestamp":"{{ fakeit_Date }}"}}' \
-y
```
### YAML Configuration
```yaml
method: "POST"
url: "https://api.realtime-app.com/events"
yes: true
timeout: "5s"
dodos: 15
requests: 1000
duration: "60s"
skip_verify: false
headers:
- Content-Type: "application/json"
- X-Event-Type: "{{ fakeit_LetterNN 4 12 }}"
- Connection: "keep-alive"
- Cache-Control: "no-cache"
cookies:
- connection_id: "{{ fakeit_UUID }}"
- session_token: "{{ fakeit_UUID }}"
params:
- channel: ["general", "notifications", "alerts", "updates"]
- version: ["v1", "v2"]
body:
- '{"event":"{{ fakeit_Word }}","data":{"timestamp":"{{ fakeit_Date }}","user_id":"{{ fakeit_UUID }}","message":"{{ fakeit_Sentence 8 }}"}}'
- '{"event":"ping","data":{"timestamp":"{{ fakeit_Date }}","client_id":"{{ fakeit_UUID }}"}}'
- '{"event":"status_update","data":{"status":"{{ fakeit_Word }}","user_id":"{{ fakeit_UUID }}","timestamp":"{{ fakeit_Date }}"}}'
```
### JSON Configuration
```json
{
"method": "POST",
"url": "https://api.realtime-app.com/events",
"yes": true,
"timeout": "5s",
"dodos": 15,
"requests": 1000,
"duration": "60s",
"skip_verify": false,
"headers": [
{ "Content-Type": "application/json" },
{ "X-Event-Type": "{{ fakeit_LetterNN 4 12 }}" },
{ "Connection": "keep-alive" },
{ "Cache-Control": "no-cache" }
],
"cookies": [
{ "connection_id": "{{ fakeit_UUID }}" },
{ "session_token": "{{ fakeit_UUID }}" }
],
"params": [
{ "channel": ["general", "notifications", "alerts", "updates"] },
{ "version": ["v1", "v2"] }
],
"body": [
"{\"event\":\"{{ fakeit_Word }}\",\"data\":{\"timestamp\":\"{{ fakeit_Date }}\",\"user_id\":\"{{ fakeit_UUID }}\",\"message\":\"{{ fakeit_Sentence 8 }}\"}}",
"{\"event\":\"ping\",\"data\":{\"timestamp\":\"{{ fakeit_Date }}\",\"client_id\":\"{{ fakeit_UUID }}\"}}",
"{\"event\":\"status_update\",\"data\":{\"status\":\"{{ fakeit_Word }}\",\"user_id\":\"{{ fakeit_UUID }}\",\"timestamp\":\"{{ fakeit_Date }}\"}}"
]
}
```
---
## 11. Multi-tenant Application Testing
Test SaaS applications with tenant-specific configurations.
### CLI Usage
```bash
dodo -u https://app.saas-platform.com/api/data \
-m GET \
-d 12 \
-r 600 \
-t 15s \
--skip-verify=false \
-H "X-Tenant-ID:{{ fakeit_UUID }}" \
-H "Authorization:Bearer {{ fakeit_LetterN 64 }}" \
-H "X-Client-Type:web" -H "X-Client-Type:mobile" -H "X-Client-Type:api" \
-H "Accept:application/json" \
-c "tenant_session={{ fakeit_UUID }}" \
-c "user_role=admin" -c "user_role=user" -c "user_role=viewer" \
-c "subscription_tier=free" -c "subscription_tier=pro" -c "subscription_tier=enterprise" \
-p "page={{ fakeit_IntRange 1 10 }}" \
-p "limit={{ fakeit_IntRange 10 100 }}" \
-p "sort=created_at" -p "sort=updated_at" -p "sort=name" \
-p "order=asc" -p "order=desc" \
-p "filter_by=active" -p "filter_by=inactive" -p "filter_by=pending" \
-y
```
### YAML Configuration
```yaml
method: "GET"
url: "https://app.saas-platform.com/api/data"
yes: true
timeout: "15s"
dodos: 12
requests: 600
skip_verify: false
headers:
- X-Tenant-ID: "{{ fakeit_UUID }}"
- Authorization: "Bearer {{ fakeit_LetterN 64 }}"
- X-Client-Type: ["web", "mobile", "api"]
- Accept: "application/json"
cookies:
- tenant_session: "{{ fakeit_UUID }}"
- user_role: ["admin", "user", "viewer"]
- subscription_tier: ["free", "pro", "enterprise"]
params:
- page: "{{ fakeit_IntRange 1 10 }}"
- limit: "{{ fakeit_IntRange 10 100 }}"
- sort: ["created_at", "updated_at", "name"]
- order: ["asc", "desc"]
- filter_by: ["active", "inactive", "pending"]
```
### JSON Configuration
```json
{
"method": "GET",
"url": "https://app.saas-platform.com/api/data",
"yes": true,
"timeout": "15s",
"dodos": 12,
"requests": 600,
"skip_verify": false,
"headers": [
{ "X-Tenant-ID": "{{ fakeit_UUID }}" },
{ "Authorization": "Bearer {{ fakeit_LetterN 64 }}" },
{ "X-Client-Type": ["web", "mobile", "api"] },
{ "Accept": "application/json" }
],
"cookies": [
{ "tenant_session": "{{ fakeit_UUID }}" },
{ "user_role": ["admin", "user", "viewer"] },
{ "subscription_tier": ["free", "pro", "enterprise"] }
],
"params": [
{ "page": "{{ fakeit_IntRange 1 10 }}" },
{ "limit": "{{ fakeit_IntRange 10 100 }}" },
{ "sort": ["created_at", "updated_at", "name"] },
{ "order": ["asc", "desc"] },
{ "filter_by": ["active", "inactive", "pending"] }
]
}
```
---
## 12. Rate Limiting Testing
Test API rate limits and throttling mechanisms.
### CLI Usage
```bash
dodo -u https://api.rate-limited.com/endpoint \
-m GET \
-d 20 \
-r 2000 \
-t 3s \
-o 120s \
--skip-verify=false \
-H "X-API-Key:{{ fakeit_UUID }}" \
-H "X-Client-ID:{{ fakeit_UUID }}" \
-H "X-Rate-Limit-Test:true" \
-H "User-Agent:{{ fakeit_UserAgent }}" \
-c "rate_limit_bucket={{ fakeit_UUID }}" \
-c "client_tier=tier1" -c "client_tier=tier2" -c "client_tier=tier3" \
-p "burst_test=true" \
-p "client_type=premium" -p "client_type=standard" -p "client_type=free" \
-p "request_id={{ fakeit_UUID }}" \
-y
```
### YAML Configuration
```yaml
method: "GET"
url: "https://api.rate-limited.com/endpoint"
yes: true
timeout: "3s"
dodos: 20
requests: 2000
duration: "120s"
skip_verify: false
headers:
- X-API-Key: "{{ fakeit_UUID }}"
- X-Client-ID: "{{ fakeit_UUID }}"
- X-Rate-Limit-Test: "true"
- User-Agent: "{{ fakeit_UserAgent }}"
params:
- burst_test: "true"
- client_type: ["premium", "standard", "free"]
- request_id: "{{ fakeit_UUID }}"
cookies:
- rate_limit_bucket: "{{ fakeit_UUID }}"
- client_tier: ["tier1", "tier2", "tier3"]
```
### JSON Configuration
```json
{
"method": "GET",
"url": "https://api.rate-limited.com/endpoint",
"yes": true,
"timeout": "3s",
"dodos": 20,
"requests": 2000,
"duration": "120s",
"skip_verify": false,
"headers": [
{ "X-API-Key": "{{ fakeit_UUID }}" },
{ "X-Client-ID": "{{ fakeit_UUID }}" },
{ "X-Rate-Limit-Test": "true" },
{ "User-Agent": "{{ fakeit_UserAgent }}" }
],
"params": [
{ "burst_test": "true" },
{ "client_type": ["premium", "standard", "free"] },
{ "request_id": "{{ fakeit_UUID }}" }
],
"cookies": [
{ "rate_limit_bucket": "{{ fakeit_UUID }}" },
{ "client_tier": ["tier1", "tier2", "tier3"] }
]
}
```
---
## Notes
- All examples use template functions for dynamic data generation
- Adjust `dodos`, `requests`, `duration`, and `timeout` values based on your testing requirements
- Use `skip_verify: true` for testing with self-signed certificates
- Set `yes: true` to skip confirmation prompts in automated testing
- Template functions like `{{ fakeit_* }}` generate random realistic data for each request
- Multiple values in arrays (e.g., `["value1", "value2"]`) will be randomly selected per request
- Use the `body_FormData` function for multipart form uploads
- Proxy configurations support HTTP, SOCKS5, and SOCKS5H protocols
For more template functions and advanced configuration options, refer to the main documentation and `utils/templates.go`.

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

313
README.md
View File

@ -3,19 +3,40 @@
<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/VzPtSHS7yPQT7ngoZzZSNU/browse?path=%2Fdodo.png">
</p> </p>
<div align="center">
<h4>
<a href="./EXAMPLES.md">
Examples
</a>
<span> | </span>
<a href="#installation">
Install
</a>
<span> | </span>
<a href="https://hub.docker.com/r/aykhans/dodo">
Docker
</a>
</h4>
<br>
<a href="https://coff.ee/aykhan">
<img src="https://cdn.buymeacoffee.com/buttons/v2/default-yellow.png" alt="Buy Me A Coffee" style="height: 40px !important;width: 150px !important;">
</a>
</div>
## Table of Contents ## Table of Contents
- [Installation](#installation) - [Installation](#installation)
- [Using Docker (Recommended)](#using-docker-recommended) - [Using Docker (Recommended)](#using-docker-recommended)
- [Using Pre-built Binaries](#using-pre-built-binaries) - [Using Pre-built Binaries](#using-pre-built-binaries)
- [Building from Source](#building-from-source) - [Building from Source](#building-from-source)
- [Usage](#usage) - [Usage](#usage)
- [1. CLI Usage](#1-cli-usage) - [1. CLI Usage](#1-cli-usage)
- [2. Config File Usage](#2-config-file-usage) - [2. Config File Usage](#2-config-file-usage)
- [2.1 JSON Example](#21-json-example) - [2.1 YAML/YML Example](#21-yamlyml-example)
- [2.2 YAML/YML Example](#22-yamlyml-example) - [2.2 JSON Example](#22-json-example)
- [3. CLI & Config File Combination](#3-cli--config-file-combination) - [3. CLI & Config File Combination](#3-cli--config-file-combination)
- [Config Parameters Reference](#config-parameters-reference) - [Config Parameters Reference](#config-parameters-reference)
- [Template Functions](#template-functions)
## Installation ## Installation
@ -45,27 +66,11 @@ 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
go install -ldflags "-s -w" github.com/aykhans/dodo@latest
```sh ```
git clone https://github.com/aykhans/dodo.git
```
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
@ -73,23 +78,100 @@ Dodo supports CLI arguments, configuration files (JSON/YAML), or a combination o
### 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 YAML/YML Example
```yaml
method: "GET"
url: "https://example.com"
yes: false
timeout: "800ms"
dodos: 10
requests: 1000
duration: "250s"
skip_verify: false
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:
```sh
docker run --rm -i -v /path/to/config.yaml:/config.yaml aykhans/dodo -f /config.yaml
# OR
docker run --rm -i aykhans/dodo -f https://example.com/config.yaml
```
#### 2.2 JSON Example
```jsonc ```jsonc
{ {
@ -99,6 +181,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
@ -167,110 +251,91 @@ docker run --rm -i -v /path/to/config.json:/config.json aykhans/dodo
docker run --rm -i aykhans/dodo -f https://example.com/config.json 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
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:
```sh
docker run --rm -i -v /path/to/config.yaml:/config.yaml aykhans/dodo -f /config.yaml
# OR
docker run --rm -i aykhans/dodo -f https://example.com/config.yaml
```
### 3. CLI & Config File Combination ### 3. CLI & Config File Combination
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
``` ```
You can find more usage examples in the [EXAMPLES.md](./EXAMPLES.md) file.
## 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 | | Requests | requests | -requests | -r | UnsignedInteger | Total number of requests to send | - |
| Timeout | timeout | -timeout | -t | Duration | Timeout for canceling each request | 10s | | Duration | duration | -duration | -o | Time | Maximum duration for the test | - |
| Params | params | -param | -p | [{String: String OR [String]}] | Request parameters | - | | Timeout | timeout | -timeout | -t | Time | Timeout for canceling each request | 10s |
| Headers | headers | -header | -H | [{String: String OR [String]}] | Request headers | - | | Params | params | -param | -p | [{String: String OR [String]}] | Request parameters | - |
| Cookies | cookies | -cookie | -c | [{String: String OR [String]}] | Request cookies | - | | Headers | headers | -header | -H | [{String: String OR [String]}] | Request headers | - |
| Body | body | -body | -b | String OR [String] | Request body or list of request bodies | - | | Cookies | cookies | -cookie | -c | [{String: String OR [String]}] | Request cookies | - |
| Proxy | proxies | -proxy | -x | String OR [String] | Proxy URL or list of proxy URLs | - | | 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 | - |
| Skip Verify | skip_verify | -skip-verify | | Boolean | Skip SSL/TLS certificate verification | false |
## Template Functions
Dodo supports template functions in `Headers`, `Params`, `Cookies`, and `Body` fields. These functions allow you to generate dynamic values for each request.
You can use Go template syntax to include dynamic values in your requests. Here's how to use template functions:
In CLI config:
```sh
dodo -u https://example.com -r 1 \
-header "User-Agent:{{ fakeit_UserAgent }}" \ # e.g. "User-Agent: Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko)"
-param "username={{ strings_ToUpper fakeit_Username }}" \ # e.g. "username=JOHN BOB"
-cookie "token={{ fakeit_Password true true true true true 10 }}" \ # e.g. token=1234567890abcdef1234567890abcdef
-body '{"email":"{{ fakeit_Email }}", "password":"{{ fakeit_Password true true true true true 10 }}"}' # e.g. {"email":"john.doe@example.com", "password":"12rw4d-78d"}
```
In YAML/YML config:
```yaml
headers:
- User-Agent: "{{ fakeit_UserAgent }}" # e.g. "User-Agent: Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko)"
- "Random-Header-{{fakeit_FirstName}}": "static_value" # e.g. "Random-Header-John: static_value"
cookies:
- token: "Bearer {{ fakeit_UUID }}" # e.g. "token=Bearer 1234567890abcdef1234567890abcdef"
params:
- id: "{{ fakeit_Uint }}" # e.g. "id=1234567890"
- username: "{{ fakeit_Username }}" # e.g. "username=John Doe"
body:
- '{ "username": "{{ fakeit_Username }}", "password": "{{ fakeit_Password }}" }' # e.g. { "username": "john.doe", "password": "password123" }
- '{ "email": "{{ fakeit_Email }}", "phone": "{{ fakeit_Phone }}" }' # e.g. { "email": "john.doe@example.com", "phone": "1234567890" }
- '{{ body_FormData (dict_Str "username" fakeit_Username "password" "secret123") }}' # Creates multipart form data for form submissions, automatically sets the appropriate Content-Type header.
```
In JSON config:
```jsonc
{
"headers": [
{ "User-Agent": "{{ fakeit_UserAgent }}" }, // e.g. "User-Agent: Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko)"
],
"body": [
"{ \"username\": \"{{ strings_RemoveSpaces fakeit_Username }}\", \"password\": \"{{ fakeit_Password }}\" }", // e.g. { "username": "johndoe", "password": "password123" }
"{{ body_FormData (dict_Str \"username\" fakeit_Username \"password\" \"12345\") }}", // Creates multipart form data for form submissions, automatically sets the appropriate Content-Type header.
],
}
```
For the full list of template functions over 200 functions, refer to the `NewFuncMap` function in `utils/templates.go`.

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: CGO_ENABLED=0 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

@ -1,12 +1,15 @@
package config package config
import ( import (
"bytes"
"errors" "errors"
"fmt" "fmt"
"math/rand"
"net/url" "net/url"
"os" "os"
"slices" "slices"
"strings" "strings"
"text/template"
"time" "time"
"github.com/aykhans/dodo/types" "github.com/aykhans/dodo/types"
@ -15,13 +18,15 @@ import (
) )
const ( const (
VERSION string = "0.6.1" VERSION string = "0.7.1"
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 +37,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 +54,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 +66,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 +107,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()
t.AppendRow(table.Row{"Requests", rc.RequestCount}) if rc.RequestCount > 0 {
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 +128,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 +140,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 +154,17 @@ 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 != "http" && config.URL.Scheme != "https" {
c.URL.Scheme = "http"
}
if c.URL.Scheme != "http" && c.URL.Scheme != "https" {
errs = append(errs, errors.New("request URL scheme must be http or https")) 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) {
@ -178,6 +201,98 @@ func (c *Config) Validate() []error {
} }
} }
funcMap := *utils.NewFuncMapGenerator(
rand.New(
rand.NewSource(
time.Now().UnixNano(),
),
),
).GetFuncMap()
for _, header := range config.Headers {
t, err := template.New("default").Funcs(funcMap).Parse(header.Key)
if err != nil {
errs = append(errs, fmt.Errorf("header key (%s) parse error: %v", header.Key, err))
} else {
var buf bytes.Buffer
if err = t.Execute(&buf, nil); err != nil {
errs = append(errs, fmt.Errorf("header key (%s) parse error: %v", header.Key, err))
}
}
for _, value := range header.Value {
t, err := template.New("default").Funcs(funcMap).Parse(value)
if err != nil {
errs = append(errs, fmt.Errorf("header value (%s) parse error: %v", value, err))
} else {
var buf bytes.Buffer
if err = t.Execute(&buf, nil); err != nil {
errs = append(errs, fmt.Errorf("header value (%s) parse error: %v", value, err))
}
}
}
}
for _, cookie := range config.Cookies {
t, err := template.New("default").Funcs(funcMap).Parse(cookie.Key)
if err != nil {
errs = append(errs, fmt.Errorf("cookie key (%s) parse error: %v", cookie.Key, err))
} else {
var buf bytes.Buffer
if err = t.Execute(&buf, nil); err != nil {
errs = append(errs, fmt.Errorf("cookie key (%s) parse error: %v", cookie.Key, err))
}
}
for _, value := range cookie.Value {
t, err := template.New("default").Funcs(funcMap).Parse(value)
if err != nil {
errs = append(errs, fmt.Errorf("cookie value (%s) parse error: %v", value, err))
} else {
var buf bytes.Buffer
if err = t.Execute(&buf, nil); err != nil {
errs = append(errs, fmt.Errorf("cookie value (%s) parse error: %v", value, err))
}
}
}
}
for _, param := range config.Params {
t, err := template.New("default").Funcs(funcMap).Parse(param.Key)
if err != nil {
errs = append(errs, fmt.Errorf("param key (%s) parse error: %v", param.Key, err))
} else {
var buf bytes.Buffer
if err = t.Execute(&buf, nil); err != nil {
errs = append(errs, fmt.Errorf("param key (%s) parse error: %v", param.Key, err))
}
}
for _, value := range param.Value {
t, err := template.New("default").Funcs(funcMap).Parse(value)
if err != nil {
errs = append(errs, fmt.Errorf("param value (%s) parse error: %v", value, err))
} else {
var buf bytes.Buffer
if err = t.Execute(&buf, nil); err != nil {
errs = append(errs, fmt.Errorf("param value (%s) parse error: %v", value, err))
}
}
}
}
for _, body := range config.Body {
t, err := template.New("default").Funcs(funcMap).Parse(body)
if err != nil {
errs = append(errs, fmt.Errorf("body (%s) parse error: %v", body, err))
} else {
var buf bytes.Buffer
if err = t.Execute(&buf, nil); err != nil {
errs = append(errs, fmt.Errorf("body (%s) parse error: %v", body, err))
}
}
}
return errs return errs
} }
@ -197,9 +312,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 +351,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)
} }
} }

15
go.mod
View File

@ -1,21 +1,22 @@
module github.com/aykhans/dodo module github.com/aykhans/dodo
go 1.24.0 go 1.24.2
require ( require (
github.com/brianvoe/gofakeit/v7 v7.2.1
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
) )

26
go.sum
View File

@ -1,11 +1,13 @@
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/brianvoe/gofakeit/v7 v7.2.1 h1:AGojgaaCdgq4Adzrd2uWdbGNDyX6MWNhHdQBraNfOHI=
github.com/brianvoe/gofakeit/v7 v7.2.1/go.mod h1:QXuPeBw164PJCzCUZVmgpgHJ3Llj49jSLVkKPMtxtxA=
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/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 +19,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"
@ -39,8 +41,11 @@ 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,
@ -54,8 +59,11 @@ 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

@ -1,9 +1,11 @@
package requests package requests
import ( import (
"bytes"
"context" "context"
"math/rand" "math/rand"
"net/url" "net/url"
"text/template"
"time" "time"
"github.com/aykhans/dodo/config" "github.com/aykhans/dodo/config"
@ -21,6 +23,11 @@ type Request struct {
getRequest RequestGeneratorFunc getRequest RequestGeneratorFunc
} }
type keyValueGenerator struct {
key func() string
value func() string
}
// Send sends the HTTP request using the fasthttp client with a specified timeout. // Send sends the HTTP request using the fasthttp client with a specified timeout.
// It returns the HTTP response or an error if the request fails or times out. // It returns the HTTP response or an error if the request fails or times out.
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) {
@ -101,26 +108,28 @@ func getRequestGeneratorFunc(
bodies []string, bodies []string,
localRand *rand.Rand, localRand *rand.Rand,
) RequestGeneratorFunc { ) RequestGeneratorFunc {
bodiesLen := len(bodies)
getBody := func() string { return "" }
if bodiesLen == 1 {
getBody = func() string { return bodies[0] }
} else if bodiesLen > 1 {
getBody = utils.RandomValueCycle(bodies, localRand)
}
getParams := getKeyValueGeneratorFunc(params, localRand) getParams := getKeyValueGeneratorFunc(params, localRand)
getHeaders := getKeyValueGeneratorFunc(headers, localRand) getHeaders := getKeyValueGeneratorFunc(headers, localRand)
getCookies := getKeyValueGeneratorFunc(cookies, localRand) getCookies := getKeyValueGeneratorFunc(cookies, localRand)
getBody := getBodyValueFunc(bodies, utils.NewFuncMapGenerator(localRand), localRand)
return func() *fasthttp.Request { return func() *fasthttp.Request {
body, contentType := getBody()
headers := getHeaders()
if contentType != "" {
headers = append(headers, types.KeyValue[string, string]{
Key: "Content-Type",
Value: contentType,
})
}
return newFasthttpRequest( return newFasthttpRequest(
URL, URL,
getParams(), getParams(),
getHeaders(), headers,
getCookies(), getCookies(),
method, method,
getBody(), body,
) )
} }
} }
@ -166,9 +175,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.
@ -202,49 +208,134 @@ func getKeyValueGeneratorFunc[
keyValueSlice []types.KeyValue[string, []string], keyValueSlice []types.KeyValue[string, []string],
localRand *rand.Rand, localRand *rand.Rand,
) func() T { ) func() T {
getKeyValueSlice := []map[string]func() string{} keyValueGenerators := make([]keyValueGenerator, len(keyValueSlice))
isRandom := false
for _, kv := range keyValueSlice { funcMap := *utils.NewFuncMapGenerator(localRand).GetFuncMap()
valuesLen := len(kv.Value)
getValueFunc := func() string { return "" } for i, kv := range keyValueSlice {
if valuesLen == 1 { keyValueGenerators[i] = keyValueGenerator{
getValueFunc = func() string { return kv.Value[0] } key: getKeyFunc(kv.Key, funcMap),
} else if valuesLen > 1 { value: getValueFunc(kv.Value, funcMap, localRand),
getValueFunc = utils.RandomValueCycle(kv.Value, localRand)
isRandom = true
} }
getKeyValueSlice = append(
getKeyValueSlice,
map[string]func() string{kv.Key: getValueFunc},
)
} }
if isRandom { return func() T {
return func() T { keyValues := make(T, len(keyValueGenerators))
keyValues := make(T, len(getKeyValueSlice)) for i, keyValue := range keyValueGenerators {
for i, keyValue := range getKeyValueSlice { keyValues[i] = types.KeyValue[string, string]{
for key, value := range keyValue { Key: keyValue.key(),
keyValues[i] = types.KeyValue[string, string]{ Value: keyValue.value(),
Key: key,
Value: value(),
}
}
}
return keyValues
}
} else {
keyValues := make(T, len(getKeyValueSlice))
for i, keyValue := range getKeyValueSlice {
for key, value := range keyValue {
keyValues[i] = types.KeyValue[string, string]{
Key: key,
Value: value(),
}
} }
} }
return func() T { return keyValues } return keyValues
}
}
// getKeyFunc creates a function that processes a key string through Go's template engine.
// It takes a key string and a template.FuncMap containing the available template functions.
//
// The returned function, when called, will execute the template with the given key and return
// the processed string result. If template parsing fails, the returned function will always
// return an empty string.
//
// This enables dynamic generation of keys that can include template directives and functions.
func getKeyFunc(key string, funcMap template.FuncMap) func() string {
t, err := template.New("default").Funcs(funcMap).Parse(key)
if err != nil {
return func() string { return "" }
}
return func() string {
var buf bytes.Buffer
_ = t.Execute(&buf, nil)
return buf.String()
}
}
// getValueFunc creates a function that randomly selects and processes a value from a slice of strings
// through Go's template engine.
//
// Parameters:
// - values: A slice of string templates that can contain template directives
// - funcMap: A template.FuncMap containing all available template functions
// - localRand: A random number generator for consistent randomization
//
// The returned function, when called, will:
// 1. Select a random template from the values slice
// 2. Execute the selected template
// 3. Return the processed string result
//
// If a selected template is nil (due to earlier parsing failure), the function will return an empty string.
// This enables dynamic generation of values with randomized selection from multiple templates.
func getValueFunc(
values []string,
funcMap template.FuncMap,
localRand *rand.Rand,
) func() string {
templates := make([]*template.Template, len(values))
for i, value := range values {
t, err := template.New("default").Funcs(funcMap).Parse(value)
if err != nil {
templates[i] = nil
}
templates[i] = t
}
randomTemplateFunc := utils.RandomValueCycle(templates, localRand)
return func() string {
if tmpl := randomTemplateFunc(); tmpl == nil {
return ""
} else {
var buf bytes.Buffer
_ = tmpl.Execute(&buf, nil)
return buf.String()
}
}
}
// getBodyValueFunc creates a function that randomly selects and processes a request body from a slice of templates.
// It returns a closure that generates both the body content and the appropriate Content-Type header value.
//
// Parameters:
// - values: A slice of string templates that can contain template directives for request bodies
// - funcMapGenerator: Provides template functions and content type information
// - localRand: A random number generator for consistent randomization
//
// The returned function, when called, will:
// 1. Select a random body template from the values slice
// 2. Execute the selected template with available template functions
// 3. Return both the processed body string and the appropriate Content-Type header value
//
// If the selected template is nil (due to earlier parsing failure), the function will return
// empty strings for both the body and Content-Type.
//
// This enables dynamic generation of request bodies with proper content type headers.
func getBodyValueFunc(
values []string,
funcMapGenerator *utils.FuncMapGenerator,
localRand *rand.Rand,
) func() (string, string) {
templates := make([]*template.Template, len(values))
for i, value := range values {
t, err := template.New("default").Funcs(*funcMapGenerator.GetFuncMap()).Parse(value)
if err != nil {
templates[i] = nil
}
templates[i] = t
}
randomTemplateFunc := utils.RandomValueCycle(templates, localRand)
return func() (string, string) {
if tmpl := randomTemplateFunc(); tmpl == nil {
return "", ""
} else {
var buf bytes.Buffer
_ = tmpl.Execute(&buf, nil)
return buf.String(), funcMapGenerator.GetBodyDataHeader()
}
} }
} }

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,47 +65,60 @@ 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(ctx)
go streamProgress(streamCtx, &streamWG, int64(requestConfig.RequestCount), "Dodos Working🔥", increase) go streamProgress(streamCtx, &streamWG, requestConfig.RequestCount, "Dodos Working🔥", increase)
for i := range dodosCount { if requestConfig.RequestCount == 0 {
if i+1 == dodosCount { for i := range dodosCount {
requestCountPerDodo = requestConfig.RequestCount - (i * requestConfig.RequestCount / dodosCount) go sendRequest(
} else { ctx,
requestCountPerDodo = ((i + 1) * requestConfig.RequestCount / dodosCount) - newRequest(*requestConfig, clients, int64(i)),
(i * requestConfig.RequestCount / dodosCount) requestConfig.Timeout,
&responses[i],
increase,
&wg,
)
} }
} else {
for i := range dodosCount {
if i+1 == dodosCount {
requestCountPerDodo = requestConfig.RequestCount - (i * requestConfig.RequestCount / dodosCount)
} else {
requestCountPerDodo = ((i + 1) * 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,
requestCountPerDodo, requestCountPerDodo,
&responses[i], &responses[i],
increase, increase,
&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

@ -10,32 +10,33 @@ func Flatten[T any](nested [][]*T) []*T {
return flattened return flattened
} }
// RandomValueCycle returns a function that cycles through the provided slice of values // RandomValueCycle returns a function that cycles through the provided values in a pseudo-random order.
// in a random order. Each call to the returned function will yield a value from the slice. // Each value in the input slice will be returned before any value is repeated.
// The order of values is determined by the provided random number generator. // If the input slice is empty, the returned function will always return the zero value of type T.
// // If the input slice contains only one element, that element is always returned.
// The returned function will cycle through the values in a random order until all values // This function is not thread-safe and should not be called concurrently.
// have been returned at least once. After all values have been returned, the function will func RandomValueCycle[T any](values []T, localRand *rand.Rand) func() T {
// reset and start cycling through the values in a random order again. switch valuesLen := len(values); valuesLen {
// The returned function isn't thread-safe and should be used in a single-threaded context. case 0:
func RandomValueCycle[Value any](values []Value, localRand *rand.Rand) func() Value { var zero T
var ( return func() T { return zero }
clientsCount int = len(values) case 1:
currentIndex int = localRand.Intn(clientsCount) return func() T { return values[0] }
stopIndex int = currentIndex default:
) currentIndex := localRand.Intn(valuesLen)
stopIndex := currentIndex
return func() T {
value := values[currentIndex]
currentIndex++
if currentIndex == valuesLen {
currentIndex = 0
}
if currentIndex == stopIndex {
currentIndex = localRand.Intn(valuesLen)
stopIndex = currentIndex
}
return func() Value { return value
client := values[currentIndex]
currentIndex++
if currentIndex == clientsCount {
currentIndex = 0
} }
if currentIndex == stopIndex {
currentIndex = localRand.Intn(clientsCount)
stopIndex = currentIndex
}
return client
} }
} }

479
utils/templates.go Normal file
View File

@ -0,0 +1,479 @@
package utils
import (
"bytes"
"math/rand"
"mime/multipart"
"strings"
"text/template"
"time"
"github.com/brianvoe/gofakeit/v7"
)
type FuncMapGenerator struct {
bodyDataHeader string
localFaker *gofakeit.Faker
funcMap *template.FuncMap
}
func NewFuncMapGenerator(localRand *rand.Rand) *FuncMapGenerator {
f := &FuncMapGenerator{
localFaker: gofakeit.NewFaker(localRand, false),
}
f.funcMap = f.newFuncMap()
return f
}
func (g *FuncMapGenerator) GetBodyDataHeader() string {
tempHeader := g.bodyDataHeader
g.bodyDataHeader = ""
return tempHeader
}
func (g *FuncMapGenerator) GetFuncMap() *template.FuncMap {
return g.funcMap
}
// NewFuncMap creates a template.FuncMap populated with string manipulation functions
// and data generation functions from gofakeit.
//
// It takes a random number generator that is used to initialize a localized faker
// instance, ensuring that random data generation is deterministic within a request context.
//
// All functions are prefixed to avoid naming conflicts:
// - String functions: "strings_*"
// - Dict functions: "dict_*"
// - Body functions: "body_*"
// - Data generation functions: "fakeit_*"
func (g *FuncMapGenerator) newFuncMap() *template.FuncMap {
return &template.FuncMap{
// Strings
"strings_ToUpper": strings.ToUpper,
"strings_ToLower": strings.ToLower,
"strings_RemoveSpaces": func(s string) string { return strings.ReplaceAll(s, " ", "") },
"strings_Replace": strings.Replace,
"strings_ToDate": func(dateString string) time.Time {
date, err := time.Parse("2006-01-02", dateString)
if err != nil {
return time.Now()
}
return date
},
"strings_First": func(s string, n int) string {
if n >= len(s) {
return s
}
return s[:n]
},
"strings_Last": func(s string, n int) string {
if n >= len(s) {
return s
}
return s[len(s)-n:]
},
"strings_Truncate": func(s string, n int) string {
if n >= len(s) {
return s
}
return s[:n] + "..."
},
"strings_TrimPrefix": strings.TrimPrefix,
"strings_TrimSuffix": strings.TrimSuffix,
"strings_Join": func(sep string, values ...string) string {
return strings.Join(values, sep)
},
// Dict
"dict_Str": func(values ...string) map[string]string {
dict := make(map[string]string)
for i := 0; i < len(values); i += 2 {
if i+1 < len(values) {
key := values[i]
value := values[i+1]
dict[key] = value
}
}
return dict
},
// Slice
"slice_Str": func(values ...string) []string { return values },
"slice_Int": func(values ...int) []int { return values },
"slice_Uint": func(values ...uint) []uint { return values },
// Body
"body_FormData": func(kv map[string]string) string {
var data bytes.Buffer
writer := multipart.NewWriter(&data)
for k, v := range kv {
_ = writer.WriteField(k, v)
}
_ = writer.Close()
g.bodyDataHeader = writer.FormDataContentType()
return data.String()
},
// FakeIt / Product
"fakeit_ProductName": g.localFaker.ProductName,
"fakeit_ProductDescription": g.localFaker.ProductDescription,
"fakeit_ProductCategory": g.localFaker.ProductCategory,
"fakeit_ProductFeature": g.localFaker.ProductFeature,
"fakeit_ProductMaterial": g.localFaker.ProductMaterial,
"fakeit_ProductUPC": g.localFaker.ProductUPC,
"fakeit_ProductAudience": g.localFaker.ProductAudience,
"fakeit_ProductDimension": g.localFaker.ProductDimension,
"fakeit_ProductUseCase": g.localFaker.ProductUseCase,
"fakeit_ProductBenefit": g.localFaker.ProductBenefit,
"fakeit_ProductSuffix": g.localFaker.ProductSuffix,
// FakeIt / Person
"fakeit_Name": g.localFaker.Name,
"fakeit_NamePrefix": g.localFaker.NamePrefix,
"fakeit_NameSuffix": g.localFaker.NameSuffix,
"fakeit_FirstName": g.localFaker.FirstName,
"fakeit_MiddleName": g.localFaker.MiddleName,
"fakeit_LastName": g.localFaker.LastName,
"fakeit_Gender": g.localFaker.Gender,
"fakeit_SSN": g.localFaker.SSN,
"fakeit_Hobby": g.localFaker.Hobby,
"fakeit_Email": g.localFaker.Email,
"fakeit_Phone": g.localFaker.Phone,
"fakeit_PhoneFormatted": g.localFaker.PhoneFormatted,
// FakeIt / Auth
"fakeit_Username": g.localFaker.Username,
"fakeit_Password": g.localFaker.Password,
// FakeIt / Address
"fakeit_City": g.localFaker.City,
"fakeit_Country": g.localFaker.Country,
"fakeit_CountryAbr": g.localFaker.CountryAbr,
"fakeit_State": g.localFaker.State,
"fakeit_StateAbr": g.localFaker.StateAbr,
"fakeit_Street": g.localFaker.Street,
"fakeit_StreetName": g.localFaker.StreetName,
"fakeit_StreetNumber": g.localFaker.StreetNumber,
"fakeit_StreetPrefix": g.localFaker.StreetPrefix,
"fakeit_StreetSuffix": g.localFaker.StreetSuffix,
"fakeit_Zip": g.localFaker.Zip,
"fakeit_Latitude": g.localFaker.Latitude,
"fakeit_LatitudeInRange": func(min, max float64) float64 {
value, err := g.localFaker.LatitudeInRange(min, max)
if err != nil {
var zero float64
return zero
}
return value
},
"fakeit_Longitude": g.localFaker.Longitude,
"fakeit_LongitudeInRange": func(min, max float64) float64 {
value, err := g.localFaker.LongitudeInRange(min, max)
if err != nil {
var zero float64
return zero
}
return value
},
// FakeIt / Game
"fakeit_Gamertag": g.localFaker.Gamertag,
// FakeIt / Beer
"fakeit_BeerAlcohol": g.localFaker.BeerAlcohol,
"fakeit_BeerBlg": g.localFaker.BeerBlg,
"fakeit_BeerHop": g.localFaker.BeerHop,
"fakeit_BeerIbu": g.localFaker.BeerIbu,
"fakeit_BeerMalt": g.localFaker.BeerMalt,
"fakeit_BeerName": g.localFaker.BeerName,
"fakeit_BeerStyle": g.localFaker.BeerStyle,
"fakeit_BeerYeast": g.localFaker.BeerYeast,
// FakeIt / Car
"fakeit_CarMaker": g.localFaker.CarMaker,
"fakeit_CarModel": g.localFaker.CarModel,
"fakeit_CarType": g.localFaker.CarType,
"fakeit_CarFuelType": g.localFaker.CarFuelType,
"fakeit_CarTransmissionType": g.localFaker.CarTransmissionType,
// FakeIt / Words
"fakeit_Noun": g.localFaker.Noun,
"fakeit_NounCommon": g.localFaker.NounCommon,
"fakeit_NounConcrete": g.localFaker.NounConcrete,
"fakeit_NounAbstract": g.localFaker.NounAbstract,
"fakeit_NounCollectivePeople": g.localFaker.NounCollectivePeople,
"fakeit_NounCollectiveAnimal": g.localFaker.NounCollectiveAnimal,
"fakeit_NounCollectiveThing": g.localFaker.NounCollectiveThing,
"fakeit_NounCountable": g.localFaker.NounCountable,
"fakeit_NounUncountable": g.localFaker.NounUncountable,
"fakeit_Verb": g.localFaker.Verb,
"fakeit_VerbAction": g.localFaker.VerbAction,
"fakeit_VerbLinking": g.localFaker.VerbLinking,
"fakeit_VerbHelping": g.localFaker.VerbHelping,
"fakeit_Adverb": g.localFaker.Adverb,
"fakeit_AdverbManner": g.localFaker.AdverbManner,
"fakeit_AdverbDegree": g.localFaker.AdverbDegree,
"fakeit_AdverbPlace": g.localFaker.AdverbPlace,
"fakeit_AdverbTimeDefinite": g.localFaker.AdverbTimeDefinite,
"fakeit_AdverbTimeIndefinite": g.localFaker.AdverbTimeIndefinite,
"fakeit_AdverbFrequencyDefinite": g.localFaker.AdverbFrequencyDefinite,
"fakeit_AdverbFrequencyIndefinite": g.localFaker.AdverbFrequencyIndefinite,
"fakeit_Preposition": g.localFaker.Preposition,
"fakeit_PrepositionSimple": g.localFaker.PrepositionSimple,
"fakeit_PrepositionDouble": g.localFaker.PrepositionDouble,
"fakeit_PrepositionCompound": g.localFaker.PrepositionCompound,
"fakeit_Adjective": g.localFaker.Adjective,
"fakeit_AdjectiveDescriptive": g.localFaker.AdjectiveDescriptive,
"fakeit_AdjectiveQuantitative": g.localFaker.AdjectiveQuantitative,
"fakeit_AdjectiveProper": g.localFaker.AdjectiveProper,
"fakeit_AdjectiveDemonstrative": g.localFaker.AdjectiveDemonstrative,
"fakeit_AdjectivePossessive": g.localFaker.AdjectivePossessive,
"fakeit_AdjectiveInterrogative": g.localFaker.AdjectiveInterrogative,
"fakeit_AdjectiveIndefinite": g.localFaker.AdjectiveIndefinite,
"fakeit_Pronoun": g.localFaker.Pronoun,
"fakeit_PronounPersonal": g.localFaker.PronounPersonal,
"fakeit_PronounObject": g.localFaker.PronounObject,
"fakeit_PronounPossessive": g.localFaker.PronounPossessive,
"fakeit_PronounReflective": g.localFaker.PronounReflective,
"fakeit_PronounDemonstrative": g.localFaker.PronounDemonstrative,
"fakeit_PronounInterrogative": g.localFaker.PronounInterrogative,
"fakeit_PronounRelative": g.localFaker.PronounRelative,
"fakeit_Connective": g.localFaker.Connective,
"fakeit_ConnectiveTime": g.localFaker.ConnectiveTime,
"fakeit_ConnectiveComparative": g.localFaker.ConnectiveComparative,
"fakeit_ConnectiveComplaint": g.localFaker.ConnectiveComplaint,
"fakeit_ConnectiveListing": g.localFaker.ConnectiveListing,
"fakeit_ConnectiveCasual": g.localFaker.ConnectiveCasual,
"fakeit_ConnectiveExamplify": g.localFaker.ConnectiveExamplify,
"fakeit_Word": g.localFaker.Word,
"fakeit_Sentence": g.localFaker.Sentence,
"fakeit_Paragraph": g.localFaker.Paragraph,
"fakeit_LoremIpsumWord": g.localFaker.LoremIpsumWord,
"fakeit_LoremIpsumSentence": g.localFaker.LoremIpsumSentence,
"fakeit_LoremIpsumParagraph": g.localFaker.LoremIpsumParagraph,
"fakeit_Question": g.localFaker.Question,
"fakeit_Quote": g.localFaker.Quote,
"fakeit_Phrase": g.localFaker.Phrase,
// FakeIt / Foods
"fakeit_Fruit": g.localFaker.Fruit,
"fakeit_Vegetable": g.localFaker.Vegetable,
"fakeit_Breakfast": g.localFaker.Breakfast,
"fakeit_Lunch": g.localFaker.Lunch,
"fakeit_Dinner": g.localFaker.Dinner,
"fakeit_Snack": g.localFaker.Snack,
"fakeit_Dessert": g.localFaker.Dessert,
// FakeIt / Misc
"fakeit_Bool": g.localFaker.Bool,
"fakeit_UUID": g.localFaker.UUID,
"fakeit_FlipACoin": g.localFaker.FlipACoin,
// FakeIt / Colors
"fakeit_Color": g.localFaker.Color,
"fakeit_HexColor": g.localFaker.HexColor,
"fakeit_RGBColor": g.localFaker.RGBColor,
"fakeit_SafeColor": g.localFaker.SafeColor,
"fakeit_NiceColors": g.localFaker.NiceColors,
// FakeIt / Internet
"fakeit_URL": g.localFaker.URL,
"fakeit_DomainName": g.localFaker.DomainName,
"fakeit_DomainSuffix": g.localFaker.DomainSuffix,
"fakeit_IPv4Address": g.localFaker.IPv4Address,
"fakeit_IPv6Address": g.localFaker.IPv6Address,
"fakeit_MacAddress": g.localFaker.MacAddress,
"fakeit_HTTPStatusCode": g.localFaker.HTTPStatusCode,
"fakeit_HTTPStatusCodeSimple": g.localFaker.HTTPStatusCodeSimple,
"fakeit_LogLevel": g.localFaker.LogLevel,
"fakeit_HTTPMethod": g.localFaker.HTTPMethod,
"fakeit_HTTPVersion": g.localFaker.HTTPVersion,
"fakeit_UserAgent": g.localFaker.UserAgent,
"fakeit_ChromeUserAgent": g.localFaker.ChromeUserAgent,
"fakeit_FirefoxUserAgent": g.localFaker.FirefoxUserAgent,
"fakeit_OperaUserAgent": g.localFaker.OperaUserAgent,
"fakeit_SafariUserAgent": g.localFaker.SafariUserAgent,
// FakeIt / HTML
"fakeit_InputName": g.localFaker.InputName,
// FakeIt / Date/Time
"fakeit_Date": g.localFaker.Date,
"fakeit_PastDate": g.localFaker.PastDate,
"fakeit_FutureDate": g.localFaker.FutureDate,
"fakeit_DateRange": g.localFaker.DateRange,
"fakeit_NanoSecond": g.localFaker.NanoSecond,
"fakeit_Second": g.localFaker.Second,
"fakeit_Minute": g.localFaker.Minute,
"fakeit_Hour": g.localFaker.Hour,
"fakeit_Month": g.localFaker.Month,
"fakeit_MonthString": g.localFaker.MonthString,
"fakeit_Day": g.localFaker.Day,
"fakeit_WeekDay": g.localFaker.WeekDay,
"fakeit_Year": g.localFaker.Year,
"fakeit_TimeZone": g.localFaker.TimeZone,
"fakeit_TimeZoneAbv": g.localFaker.TimeZoneAbv,
"fakeit_TimeZoneFull": g.localFaker.TimeZoneFull,
"fakeit_TimeZoneOffset": g.localFaker.TimeZoneOffset,
"fakeit_TimeZoneRegion": g.localFaker.TimeZoneRegion,
// FakeIt / Payment
"fakeit_Price": g.localFaker.Price,
"fakeit_CreditCardCvv": g.localFaker.CreditCardCvv,
"fakeit_CreditCardExp": g.localFaker.CreditCardExp,
"fakeit_CreditCardNumber": g.localFaker.CreditCardNumber,
"fakeit_CreditCardType": g.localFaker.CreditCardType,
"fakeit_CurrencyLong": g.localFaker.CurrencyLong,
"fakeit_CurrencyShort": g.localFaker.CurrencyShort,
"fakeit_AchRouting": g.localFaker.AchRouting,
"fakeit_AchAccount": g.localFaker.AchAccount,
"fakeit_BitcoinAddress": g.localFaker.BitcoinAddress,
"fakeit_BitcoinPrivateKey": g.localFaker.BitcoinPrivateKey,
// FakeIt / Finance
"fakeit_Cusip": g.localFaker.Cusip,
"fakeit_Isin": g.localFaker.Isin,
// FakeIt / Company
"fakeit_BS": g.localFaker.BS,
"fakeit_Blurb": g.localFaker.Blurb,
"fakeit_BuzzWord": g.localFaker.BuzzWord,
"fakeit_Company": g.localFaker.Company,
"fakeit_CompanySuffix": g.localFaker.CompanySuffix,
"fakeit_JobDescriptor": g.localFaker.JobDescriptor,
"fakeit_JobLevel": g.localFaker.JobLevel,
"fakeit_JobTitle": g.localFaker.JobTitle,
"fakeit_Slogan": g.localFaker.Slogan,
// FakeIt / Hacker
"fakeit_HackerAbbreviation": g.localFaker.HackerAbbreviation,
"fakeit_HackerAdjective": g.localFaker.HackerAdjective,
"fakeit_HackerNoun": g.localFaker.HackerNoun,
"fakeit_HackerPhrase": g.localFaker.HackerPhrase,
"fakeit_HackerVerb": g.localFaker.HackerVerb,
// FakeIt / Hipster
"fakeit_HipsterWord": g.localFaker.HipsterWord,
"fakeit_HipsterSentence": g.localFaker.HipsterSentence,
"fakeit_HipsterParagraph": g.localFaker.HipsterParagraph,
// FakeIt / App
"fakeit_AppName": g.localFaker.AppName,
"fakeit_AppVersion": g.localFaker.AppVersion,
"fakeit_AppAuthor": g.localFaker.AppAuthor,
// FakeIt / Animal
"fakeit_PetName": g.localFaker.PetName,
"fakeit_Animal": g.localFaker.Animal,
"fakeit_AnimalType": g.localFaker.AnimalType,
"fakeit_FarmAnimal": g.localFaker.FarmAnimal,
"fakeit_Cat": g.localFaker.Cat,
"fakeit_Dog": g.localFaker.Dog,
"fakeit_Bird": g.localFaker.Bird,
// FakeIt / Emoji
"fakeit_Emoji": g.localFaker.Emoji,
"fakeit_EmojiDescription": g.localFaker.EmojiDescription,
"fakeit_EmojiCategory": g.localFaker.EmojiCategory,
"fakeit_EmojiAlias": g.localFaker.EmojiAlias,
"fakeit_EmojiTag": g.localFaker.EmojiTag,
// FakeIt / Language
"fakeit_Language": g.localFaker.Language,
"fakeit_LanguageAbbreviation": g.localFaker.LanguageAbbreviation,
"fakeit_ProgrammingLanguage": g.localFaker.ProgrammingLanguage,
// FakeIt / Number
"fakeit_Number": g.localFaker.Number,
"fakeit_Int": g.localFaker.Int,
"fakeit_IntN": g.localFaker.IntN,
"fakeit_IntRange": g.localFaker.IntRange,
"fakeit_RandomInt": g.localFaker.RandomInt,
"fakeit_Int8": g.localFaker.Int8,
"fakeit_Int16": g.localFaker.Int16,
"fakeit_Int32": g.localFaker.Int32,
"fakeit_Int64": g.localFaker.Int64,
"fakeit_Uint": g.localFaker.Uint,
"fakeit_UintN": g.localFaker.UintN,
"fakeit_UintRange": g.localFaker.UintRange,
"fakeit_RandomUint": g.localFaker.RandomUint,
"fakeit_Uint8": g.localFaker.Uint8,
"fakeit_Uint16": g.localFaker.Uint16,
"fakeit_Uint32": g.localFaker.Uint32,
"fakeit_Uint64": g.localFaker.Uint64,
"fakeit_Float32": g.localFaker.Float32,
"fakeit_Float32Range": g.localFaker.Float32Range,
"fakeit_Float64": g.localFaker.Float64,
"fakeit_Float64Range": g.localFaker.Float64Range,
"fakeit_HexUint": g.localFaker.HexUint,
// FakeIt / String
"fakeit_Digit": g.localFaker.Digit,
"fakeit_DigitN": g.localFaker.DigitN,
"fakeit_Letter": g.localFaker.Letter,
"fakeit_LetterN": g.localFaker.LetterN,
"fakeit_LetterNN": func(min, max uint) string {
return g.localFaker.LetterN(g.localFaker.UintRange(min, max))
},
"fakeit_Lexify": g.localFaker.Lexify,
"fakeit_Numerify": g.localFaker.Numerify,
"fakeit_RandomString": func(values ...string) string {
return g.localFaker.RandomString(values)
},
// FakeIt / Celebrity
"fakeit_CelebrityActor": g.localFaker.CelebrityActor,
"fakeit_CelebrityBusiness": g.localFaker.CelebrityBusiness,
"fakeit_CelebritySport": g.localFaker.CelebritySport,
// FakeIt / Minecraft
"fakeit_MinecraftOre": g.localFaker.MinecraftOre,
"fakeit_MinecraftWood": g.localFaker.MinecraftWood,
"fakeit_MinecraftArmorTier": g.localFaker.MinecraftArmorTier,
"fakeit_MinecraftArmorPart": g.localFaker.MinecraftArmorPart,
"fakeit_MinecraftWeapon": g.localFaker.MinecraftWeapon,
"fakeit_MinecraftTool": g.localFaker.MinecraftTool,
"fakeit_MinecraftDye": g.localFaker.MinecraftDye,
"fakeit_MinecraftFood": g.localFaker.MinecraftFood,
"fakeit_MinecraftAnimal": g.localFaker.MinecraftAnimal,
"fakeit_MinecraftVillagerJob": g.localFaker.MinecraftVillagerJob,
"fakeit_MinecraftVillagerStation": g.localFaker.MinecraftVillagerStation,
"fakeit_MinecraftVillagerLevel": g.localFaker.MinecraftVillagerLevel,
"fakeit_MinecraftMobPassive": g.localFaker.MinecraftMobPassive,
"fakeit_MinecraftMobNeutral": g.localFaker.MinecraftMobNeutral,
"fakeit_MinecraftMobHostile": g.localFaker.MinecraftMobHostile,
"fakeit_MinecraftMobBoss": g.localFaker.MinecraftMobBoss,
"fakeit_MinecraftBiome": g.localFaker.MinecraftBiome,
"fakeit_MinecraftWeather": g.localFaker.MinecraftWeather,
// FakeIt / Book
"fakeit_BookTitle": g.localFaker.BookTitle,
"fakeit_BookAuthor": g.localFaker.BookAuthor,
"fakeit_BookGenre": g.localFaker.BookGenre,
// FakeIt / Movie
"fakeit_MovieName": g.localFaker.MovieName,
"fakeit_MovieGenre": g.localFaker.MovieGenre,
// FakeIt / Error
"fakeit_Error": g.localFaker.Error,
"fakeit_ErrorDatabase": g.localFaker.ErrorDatabase,
"fakeit_ErrorGRPC": g.localFaker.ErrorGRPC,
"fakeit_ErrorHTTP": g.localFaker.ErrorHTTP,
"fakeit_ErrorHTTPClient": g.localFaker.ErrorHTTPClient,
"fakeit_ErrorHTTPServer": g.localFaker.ErrorHTTPServer,
"fakeit_ErrorRuntime": g.localFaker.ErrorRuntime,
// FakeIt / School
"fakeit_School": g.localFaker.School,
// FakeIt / Song
"fakeit_SongName": g.localFaker.SongName,
"fakeit_SongArtist": g.localFaker.SongArtist,
"fakeit_SongGenre": g.localFaker.SongGenre,
}
}