mirror of
				https://github.com/aykhans/dodo.git
				synced 2025-11-03 14:09:58 +00:00 
			
		
		
		
	Compare commits
	
		
			150 Commits
		
	
	
		
			ff09a3365e
			...
			dependabot
		
	
	| Author | SHA1 | Date | |
|---|---|---|---|
| 
						 | 
					cabe562d47 | ||
| 25d4762a3c | |||
| 361d423651 | |||
| ffa724fae7 | |||
| 7930be490d | |||
| e6c54e9cb2 | |||
| b32f567de7 | |||
| b6e85d9443 | |||
| 827e3535cd | |||
| 7ecf534d87 | |||
| 
						 | 
					17ad5fadb9 | ||
| 7fb59a7989 | |||
| 527909c882 | |||
| 4459675efa | |||
| 
						 | 
					604af355e6 | ||
| 7d4267c4c2 | |||
| 
						 | 
					845ab7296c | ||
| 49d004ff06 | |||
| 045deb6120 | |||
| 075ef26203 | |||
| 
						 | 
					946afbb2c3 | ||
| aacb33cfa5 | |||
| 4a7db48351 | |||
| b73087dce5 | |||
| 
						 | 
					20a46feab8 | ||
| 0adde6e04e | |||
| ca50de4e2f | |||
| c99e7c66d9 | |||
| 280e5f5c4e | |||
| 47dfad6046 | |||
| 5bb644d55f | |||
| 9152eefdc5 | |||
| a8cd253c63 | |||
| 9aaf2db74d | |||
| 5c3e254e1e | |||
| e5c681a22b | |||
| 79668e4ece | |||
| f248c2af96 | |||
| 924bd819ee | |||
| e567155eb1 | |||
| 23c74bdbb1 | |||
| addf92df91 | |||
| 6aeda3706b | |||
| dc1cd05714 | |||
| 2b9d0520b0 | |||
| bea2e7c040 | |||
| 
						 | 
					b52b336a52 | ||
| c927e31c49 | |||
| d8e6f532a8 | |||
| cf5cd23d97 | |||
| 
						 | 
					350ff4d66d | ||
| cb8898d20e | |||
| a552d1c9f9 | |||
| 35263f1dd6 | |||
| 930e173a6a | |||
| bea2a81afa | |||
| 53ed486b23 | |||
| 0b9c32a09d | |||
| 42d5617e3f | |||
| e80ae9ab24 | |||
| 
						 | 
					86a6f7814b | ||
| 09034b5f9e | |||
| f1ca2041c3 | |||
| f5a29a2657 | |||
| 439f66eb87 | |||
| 415d0130ce | |||
| abaa8e90b2 | |||
| 046ce74cd9 | |||
| 681cafc213 | |||
| 7e05cf4f6b | |||
| 934cd0ad33 | |||
| 69c4841a05 | |||
| 3cc165cbf4 | |||
| 59f40ad825 | |||
| a170588574 | |||
| 2a0ac390d8 | |||
| 11bb8b3fb0 | |||
| 1aadc3419a | |||
| b3af3f6ad5 | |||
| ed52fff363 | |||
| 985fc6200d | |||
| 1808865358 | |||
| 56342e49c6 | |||
| ec80569d5d | |||
| 459f7ee0dc | |||
| 3cd72855e5 | |||
| b8011ce651 | |||
| 0aeeb484e2 | |||
| fc3244dc33 | |||
| aa6ec450b8 | |||
| e31f5ad204 | |||
| de9a4bb355 | |||
| 
						 | 
					234ca01e41 | ||
| cc490143ea | |||
| a8c3efe198 | |||
| 3c2a0ee1b2 | |||
| 00f0bcb2de | |||
| 8f811e1bec | |||
| 58ea31683b | |||
| cc2a6eb367 | |||
| f721abb583 | |||
| 
						 | 
					4a9fb9fdda | ||
| 198b6c785a | |||
| 
						 | 
					9dc56709a7 | ||
| a9021bd1a4 | |||
| 48c2dc7935 | |||
| 4cb0540824 | |||
| a01bf19986 | |||
| 74dcecc8b1 | |||
| 00afca7139 | |||
| 1589fefeb8 | |||
| bbc43bbaac | |||
| 89fcf5f174 | |||
| a58f734a55 | |||
| efd5176ab9 | |||
| f0adcaf328 | |||
| 6314cf7724 | |||
| 140e570b85 | |||
| 83c5788e54 | |||
| ca74092615 | |||
| 
						 | 
					004b10ea3c | ||
| 4c9ceb1c4b | |||
| 
						 | 
					6f3df7c45b | ||
| 8cd495055d | |||
| 
						 | 
					391a5bc6ec | ||
| a69e248f8c | |||
| 
						 | 
					2634ca110c | ||
| b1612598c4 | |||
| ba79304b04 | |||
| 015cb15053 | |||
| 3dc002188e | |||
| 769c04685a | |||
| e43378a9a4 | |||
| e29e4f1bc6 | |||
| 7d2168a014 | |||
| 
						 | 
					ba53e6b7f7 | ||
| 58964e1098 | |||
| 
						 | 
					d4bf7358ff | ||
| 779d5e9b18 | |||
| 6a79d0b1d7 | |||
| 1e53b8a7fb | |||
| 0a8dbec739 | |||
| 3762890914 | |||
| ca6b3d4eb2 | |||
| 1ee06aacc3 | |||
| 3d5834a6a6 | |||
| f1521cbb74 | |||
| 40f8a1cc37 | |||
| 8a3574cd48 | |||
| 5a2a4c47b2 | 
@@ -4,6 +4,7 @@ binaries
 | 
			
		||||
dodo
 | 
			
		||||
.git
 | 
			
		||||
.gitignore
 | 
			
		||||
.golangci.yml
 | 
			
		||||
README.md
 | 
			
		||||
LICENSE
 | 
			
		||||
config.json
 | 
			
		||||
 
 | 
			
		||||
							
								
								
									
										25
									
								
								.github/workflows/golangci-lint.yml
									
									
									
									
										vendored
									
									
										Normal file
									
								
							
							
						
						
									
										25
									
								
								.github/workflows/golangci-lint.yml
									
									
									
									
										vendored
									
									
										Normal file
									
								
							@@ -0,0 +1,25 @@
 | 
			
		||||
name: golangci-lint
 | 
			
		||||
 | 
			
		||||
on:
 | 
			
		||||
  push:
 | 
			
		||||
    branches:
 | 
			
		||||
      - main
 | 
			
		||||
  pull_request:
 | 
			
		||||
 | 
			
		||||
permissions:
 | 
			
		||||
  contents: read
 | 
			
		||||
 | 
			
		||||
jobs:
 | 
			
		||||
  golangci:
 | 
			
		||||
    name: lint
 | 
			
		||||
    runs-on: ubuntu-latest
 | 
			
		||||
    steps:
 | 
			
		||||
      - uses: actions/checkout@v4
 | 
			
		||||
      - uses: actions/setup-go@v5
 | 
			
		||||
        with:
 | 
			
		||||
          go-version: stable
 | 
			
		||||
      - name: golangci-lint
 | 
			
		||||
        uses: golangci/golangci-lint-action@v7
 | 
			
		||||
        with:
 | 
			
		||||
          version: v2.4.0
 | 
			
		||||
          args: --timeout=10m --config=.golangci.yml
 | 
			
		||||
							
								
								
									
										33
									
								
								.golangci.yml
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										33
									
								
								.golangci.yml
									
									
									
									
									
										Normal file
									
								
							@@ -0,0 +1,33 @@
 | 
			
		||||
version: "2"
 | 
			
		||||
 | 
			
		||||
run:
 | 
			
		||||
    go: "1.25"
 | 
			
		||||
    concurrency: 8
 | 
			
		||||
    timeout: 10m
 | 
			
		||||
 | 
			
		||||
linters:
 | 
			
		||||
    default: none
 | 
			
		||||
    enable:
 | 
			
		||||
        - asasalint
 | 
			
		||||
        - asciicheck
 | 
			
		||||
        - errcheck
 | 
			
		||||
        - gomodguard
 | 
			
		||||
        - goprintffuncname
 | 
			
		||||
        - govet
 | 
			
		||||
        - ineffassign
 | 
			
		||||
        - misspell
 | 
			
		||||
        - nakedret
 | 
			
		||||
        - nolintlint
 | 
			
		||||
        - prealloc
 | 
			
		||||
        - reassign
 | 
			
		||||
        - staticcheck
 | 
			
		||||
        - unconvert
 | 
			
		||||
        - unused
 | 
			
		||||
        - whitespace
 | 
			
		||||
 | 
			
		||||
    settings:
 | 
			
		||||
        staticcheck:
 | 
			
		||||
            checks:
 | 
			
		||||
                - "all"
 | 
			
		||||
                - "-S1002"
 | 
			
		||||
                - "-ST1000"
 | 
			
		||||
							
								
								
									
										14
									
								
								Dockerfile
									
									
									
									
									
								
							
							
						
						
									
										14
									
								
								Dockerfile
									
									
									
									
									
								
							@@ -1,19 +1,17 @@
 | 
			
		||||
FROM golang:1.23.2-alpine AS builder
 | 
			
		||||
FROM golang:1.25-alpine AS builder
 | 
			
		||||
 | 
			
		||||
WORKDIR /dodo
 | 
			
		||||
WORKDIR /src
 | 
			
		||||
 | 
			
		||||
COPY go.mod go.sum ./
 | 
			
		||||
RUN go mod download
 | 
			
		||||
COPY . .
 | 
			
		||||
 | 
			
		||||
RUN go build -ldflags "-s -w" -o dodo
 | 
			
		||||
RUN echo "{}" > config.json
 | 
			
		||||
RUN CGO_ENABLED=0 go build -ldflags "-s -w" -o dodo
 | 
			
		||||
 | 
			
		||||
FROM gcr.io/distroless/static-debian12:latest
 | 
			
		||||
 | 
			
		||||
WORKDIR /dodo
 | 
			
		||||
WORKDIR /
 | 
			
		||||
 | 
			
		||||
COPY --from=builder /dodo/dodo /dodo/dodo
 | 
			
		||||
COPY --from=builder /dodo/config.json /dodo/config.json
 | 
			
		||||
COPY --from=builder /src/dodo /dodo
 | 
			
		||||
 | 
			
		||||
ENTRYPOINT ["./dodo", "-c", "/dodo/config.json"]
 | 
			
		||||
ENTRYPOINT ["./dodo"]
 | 
			
		||||
							
								
								
									
										934
									
								
								EXAMPLES.md
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										934
									
								
								EXAMPLES.md
									
									
									
									
									
										Normal 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`.
 | 
			
		||||
							
								
								
									
										377
									
								
								README.md
									
									
									
									
									
								
							
							
						
						
									
										377
									
								
								README.md
									
									
									
									
									
								
							@@ -1,119 +1,340 @@
 | 
			
		||||
<h1 align="center">Dodo is a simple and easy-to-use HTTP benchmarking tool.</h1>
 | 
			
		||||
<p align="center">
 | 
			
		||||
<img width="30%" height="30%" src="https://ftp.aykhans.me/web/client/pubshares/hB6VSdCnBCr8gFPeiMuCji/browse?path=%2Fdodo.png">
 | 
			
		||||
</p>
 | 
			
		||||
<h1 align="center">Dodo - A Fast and Easy-to-Use HTTP Benchmarking Tool</h1>
 | 
			
		||||
 | 
			
		||||

 | 
			
		||||
 | 
			
		||||
<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
 | 
			
		||||
 | 
			
		||||
- [Installation](#installation)
 | 
			
		||||
    - [Using Docker (Recommended)](#using-docker-recommended)
 | 
			
		||||
    - [Using Pre-built Binaries](#using-pre-built-binaries)
 | 
			
		||||
    - [Building from Source](#building-from-source)
 | 
			
		||||
- [Usage](#usage)
 | 
			
		||||
    - [1. CLI Usage](#1-cli-usage)
 | 
			
		||||
    - [2. Config File Usage](#2-config-file-usage)
 | 
			
		||||
        - [2.1 YAML/YML Example](#21-yamlyml-example)
 | 
			
		||||
        - [2.2 JSON Example](#22-json-example)
 | 
			
		||||
    - [3. CLI & Config File Combination](#3-cli--config-file-combination)
 | 
			
		||||
- [Config Parameters Reference](#config-parameters-reference)
 | 
			
		||||
- [Template Functions](#template-functions)
 | 
			
		||||
 | 
			
		||||
## Installation
 | 
			
		||||
### With Docker (Recommended)
 | 
			
		||||
Pull the Dodo image from Docker Hub:
 | 
			
		||||
 | 
			
		||||
### Using Docker (Recommended)
 | 
			
		||||
 | 
			
		||||
Pull the latest Dodo image from Docker Hub:
 | 
			
		||||
 | 
			
		||||
```sh
 | 
			
		||||
docker pull aykhans/dodo:latest
 | 
			
		||||
```
 | 
			
		||||
If you use Dodo with Docker and a config file, you must provide the config.json file as a volume to the Docker run command (not as the "-c config.json" argument), as shown in the examples in the [usage](#usage) section.
 | 
			
		||||
 | 
			
		||||
### With Binary File
 | 
			
		||||
You can grab binaries in the [releases](https://github.com/aykhans/dodo/releases) section.
 | 
			
		||||
To use Dodo with Docker and a local config file, mount the config file as a volume and pass it as an argument:
 | 
			
		||||
 | 
			
		||||
### Build from Source
 | 
			
		||||
To build Dodo from source, you need to have [Go1.22+](https://golang.org/dl/) installed. <br>
 | 
			
		||||
Follow the steps below to build dodo:
 | 
			
		||||
```sh
 | 
			
		||||
docker run -v /path/to/config.json:/config.json aykhans/dodo -f /config.json
 | 
			
		||||
```
 | 
			
		||||
 | 
			
		||||
1. **Clone the repository:**
 | 
			
		||||
If you're using a remote config file via URL, you don't need to mount a volume:
 | 
			
		||||
 | 
			
		||||
    ```sh
 | 
			
		||||
    git clone https://github.com/aykhans/dodo.git
 | 
			
		||||
    ```
 | 
			
		||||
```sh
 | 
			
		||||
docker run aykhans/dodo -f https://raw.githubusercontent.com/aykhans/dodo/main/config.yaml
 | 
			
		||||
```
 | 
			
		||||
 | 
			
		||||
2. **Navigate to the project directory:**
 | 
			
		||||
### Using Pre-built Binaries
 | 
			
		||||
 | 
			
		||||
    ```sh
 | 
			
		||||
    cd dodo
 | 
			
		||||
    ```
 | 
			
		||||
Download the latest binaries from the [releases](https://github.com/aykhans/dodo/releases) section.
 | 
			
		||||
 | 
			
		||||
3. **Build the project:**
 | 
			
		||||
### Building from Source
 | 
			
		||||
 | 
			
		||||
    ```sh
 | 
			
		||||
    go build -ldflags "-s -w" -o dodo
 | 
			
		||||
    ```
 | 
			
		||||
To build Dodo from source, ensure you have [Go 1.24+](https://golang.org/dl/) installed.
 | 
			
		||||
 | 
			
		||||
This will generate an executable named `dodo` in the project directory.
 | 
			
		||||
```sh
 | 
			
		||||
go install -ldflags "-s -w" github.com/aykhans/dodo@latest
 | 
			
		||||
```
 | 
			
		||||
 | 
			
		||||
## Usage
 | 
			
		||||
You can use Dodo with CLI arguments, a JSON config file, or both. If you use both, CLI arguments will always override JSON config arguments if there is a conflict.
 | 
			
		||||
 | 
			
		||||
### 1. CLI
 | 
			
		||||
Send 1000 GET requests to https://example.com with 10 parallel dodos (threads) and a timeout of 2000 milliseconds:
 | 
			
		||||
Dodo supports CLI arguments, configuration files (JSON/YAML), or a combination of both. If both are used, CLI arguments take precedence.
 | 
			
		||||
 | 
			
		||||
### 1. CLI Usage
 | 
			
		||||
 | 
			
		||||
Send 1000 GET requests to https://example.com with 10 parallel dodos (threads), each with a timeout of 2 seconds, within a maximum duration of 1 minute:
 | 
			
		||||
 | 
			
		||||
```sh
 | 
			
		||||
dodo -u https://example.com -m GET -d 10 -r 1000 -t 2000
 | 
			
		||||
dodo -u https://example.com -m GET -d 10 -r 1000 -o 1m -t 2s
 | 
			
		||||
```
 | 
			
		||||
 | 
			
		||||
With Docker:
 | 
			
		||||
 | 
			
		||||
```sh
 | 
			
		||||
docker run --rm -i aykhans/dodo -u https://example.com -m GET -d 10 -r 1000 -t 2000
 | 
			
		||||
docker run --rm -i aykhans/dodo -u https://example.com -m GET -d 10 -r 1000 -o 1m -t 2s
 | 
			
		||||
```
 | 
			
		||||
 | 
			
		||||
### 2. JSON config file
 | 
			
		||||
You can find an example config structure in the [config.json](https://github.com/aykhans/dodo/blob/main/config.json) file:
 | 
			
		||||
```json
 | 
			
		||||
### 2. Config File Usage
 | 
			
		||||
 | 
			
		||||
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:
 | 
			
		||||
 | 
			
		||||
#### 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
 | 
			
		||||
{
 | 
			
		||||
    "method": "GET",
 | 
			
		||||
    "url": "https://example.com",
 | 
			
		||||
    "no_proxy_check": false,
 | 
			
		||||
    "timeout": 2000,
 | 
			
		||||
    "dodos_count": 10,
 | 
			
		||||
    "request_count": 1000,
 | 
			
		||||
    "params": {},
 | 
			
		||||
    "headers": {},
 | 
			
		||||
    "cookies": {},
 | 
			
		||||
    "body": [],
 | 
			
		||||
    "proxies": [
 | 
			
		||||
        {
 | 
			
		||||
            "url": "http://example.com:8080",
 | 
			
		||||
            "username": "username",
 | 
			
		||||
            "password": "password"
 | 
			
		||||
        },
 | 
			
		||||
        {
 | 
			
		||||
            "url": "http://example.com:8080"
 | 
			
		||||
        }
 | 
			
		||||
    ]
 | 
			
		||||
    "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",
 | 
			
		||||
    ],
 | 
			
		||||
}
 | 
			
		||||
```
 | 
			
		||||
Send 1000 GET requests to https://example.com with 10 parallel dodos (threads) and a timeout of 2000 milliseconds:
 | 
			
		||||
 | 
			
		||||
```sh
 | 
			
		||||
dodo -c /path/config.json
 | 
			
		||||
dodo -f /path/config.json
 | 
			
		||||
# OR
 | 
			
		||||
dodo -f https://example.com/config.json
 | 
			
		||||
```
 | 
			
		||||
 | 
			
		||||
With Docker:
 | 
			
		||||
```sh
 | 
			
		||||
docker run --rm -i -v ./path/config.json:/dodo/config.json aykhans/dodo
 | 
			
		||||
```
 | 
			
		||||
 | 
			
		||||
### 3. Both (CLI & JSON)
 | 
			
		||||
Override the config file arguments with CLI arguments:
 | 
			
		||||
 | 
			
		||||
```sh
 | 
			
		||||
dodo -c /path/config.json -u https://example.com -m GET -d 10 -r 1000 -t 2000
 | 
			
		||||
docker run --rm -i -v /path/to/config.json:/config.json aykhans/dodo
 | 
			
		||||
# OR
 | 
			
		||||
docker run --rm -i aykhans/dodo -f https://example.com/config.json
 | 
			
		||||
```
 | 
			
		||||
 | 
			
		||||
### 3. CLI & Config File Combination
 | 
			
		||||
 | 
			
		||||
CLI arguments override config file values:
 | 
			
		||||
 | 
			
		||||
```sh
 | 
			
		||||
dodo -f /path/to/config.yaml -u https://example.com -m GET -d 10 -r 1000 -o 1m -t 5s
 | 
			
		||||
```
 | 
			
		||||
 | 
			
		||||
With Docker:
 | 
			
		||||
 | 
			
		||||
```sh
 | 
			
		||||
docker run --rm -i -v ./path/config.json:/dodo/config.json aykhans/dodo -u https://example.com -m GET -d 10 -r 1000 -t 2000
 | 
			
		||||
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
 | 
			
		||||
```
 | 
			
		||||
 | 
			
		||||
## CLI and JSON Config Parameters
 | 
			
		||||
If the Headers, Params, Cookies and Body fields have multiple values, each request will choose a random value from the list.
 | 
			
		||||
You can find more usage examples in the [EXAMPLES.md](./EXAMPLES.md) file.
 | 
			
		||||
 | 
			
		||||
| Parameter             | JSON config file | CLI Flag        | CLI Short Flag | Type                             | Description                                                         | Default     |
 | 
			
		||||
| --------------------- | ---------------- | --------------- | -------------- | -------------------------------- | ------------------------------------------------------------------- | ----------- |
 | 
			
		||||
| Config file           | -                | --config-file   | -c             | String                           | Path to the JSON config file                                        | -           |
 | 
			
		||||
| Yes                   | -                | --yes           | -y             | Boolean                          | Answer yes to all questions                                         | false       |
 | 
			
		||||
| URL                   | url              | --url           | -u             | String                           | URL to send the request to                                          | -           |
 | 
			
		||||
| Method                | method           | --method        | -m             | String                           | HTTP method                                                         | GET         |
 | 
			
		||||
| Request count         | request_count    | --request-count | -r             | Integer                          | Total number of requests to send                                    | 1000        |
 | 
			
		||||
| Dodos count (Threads) | dodos_count      | --dodos-count   | -d             | Integer                          | Number of dodos (threads) to send requests in parallel              | 1           |
 | 
			
		||||
| Timeout               | timeout          | --timeout       | -t             | Integer                          | Timeout for canceling each request (milliseconds)                   | 10000       |
 | 
			
		||||
| No Proxy Check        | no_proxy_check   | --no-proxy-check| -              | Boolean                          | Disable proxy check                                                 | false       |
 | 
			
		||||
| Params                | params           | -               | -              | Key-Value {String: [String]}     | Request parameters                                                  | -           |
 | 
			
		||||
| Headers               | headers          | -               | -              | Key-Value {String: [String]}     | Request headers                                                     | -           |
 | 
			
		||||
| Cookies               | cookies          | -               | -              | Key-Value {String: [String]}     | Request cookies                                                     | -           |
 | 
			
		||||
| Body                  | body             | -               | -              | [String]                         | Request body                                                        | -           |
 | 
			
		||||
| Proxy                 | proxies          | -               | -              | List[Key-Value {string: string}] | List of proxies (will check active proxies before sending requests) | -           |
 | 
			
		||||
## Config Parameters Reference
 | 
			
		||||
 | 
			
		||||
If `Headers`, `Params`, `Cookies`, `Body`, or `Proxy` fields have multiple values, each request will choose a random value from the list.
 | 
			
		||||
 | 
			
		||||
| Parameter       | config file | CLI Flag     | CLI Short Flag | Type                           | Description                                                 | Default |
 | 
			
		||||
| --------------- | ----------- | ------------ | -------------- | ------------------------------ | ----------------------------------------------------------- | ------- |
 | 
			
		||||
| Config file     |             | -config-file | -f             | String                         | Path to local config file or http(s) URL of the config file | -       |
 | 
			
		||||
| Yes             | yes         | -yes         | -y             | Boolean                        | Answer yes to all questions                                 | false   |
 | 
			
		||||
| URL             | url         | -url         | -u             | String                         | URL to send the request to                                  | -       |
 | 
			
		||||
| Method          | method      | -method      | -m             | String                         | HTTP method                                                 | GET     |
 | 
			
		||||
| Dodos (Threads) | dodos       | -dodos       | -d             | UnsignedInteger                | Number of dodos (threads) to send requests in parallel      | 1       |
 | 
			
		||||
| Requests        | requests    | -requests    | -r             | UnsignedInteger                | Total number of requests to send                            | -       |
 | 
			
		||||
| Duration        | duration    | -duration    | -o             | Time                           | Maximum duration for the test                               | -       |
 | 
			
		||||
| Timeout         | timeout     | -timeout     | -t             | Time                           | Timeout for canceling each request                          | 10s     |
 | 
			
		||||
| Params          | params      | -param       | -p             | [{String: String OR [String]}] | Request parameters                                          | -       |
 | 
			
		||||
| Headers         | headers     | -header      | -H             | [{String: String OR [String]}] | Request headers                                             | -       |
 | 
			
		||||
| Cookies         | cookies     | -cookie      | -c             | [{String: String OR [String]}] | Request cookies                                             | -       |
 | 
			
		||||
| Body            | body        | -body        | -b             | String OR [String]             | Request body or list of request bodies                      | -       |
 | 
			
		||||
| Proxy           | proxies     | -proxy       | -x             | String OR [String]             | Proxy URL or list of proxy URLs                             | -       |
 | 
			
		||||
| 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
									
								
							
							
						
						
									
										53
									
								
								Taskfile.yaml
									
									
									
									
									
										Normal 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"
 | 
			
		||||
							
								
								
									
										32
									
								
								build.sh
									
									
									
									
									
								
							
							
						
						
									
										32
									
								
								build.sh
									
									
									
									
									
								
							@@ -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
 | 
			
		||||
							
								
								
									
										49
									
								
								config.json
									
									
									
									
									
								
							
							
						
						
									
										49
									
								
								config.json
									
									
									
									
									
								
							@@ -1,22 +1,37 @@
 | 
			
		||||
{
 | 
			
		||||
    "method": "GET",
 | 
			
		||||
    "url": "https://example.com",
 | 
			
		||||
    "no_proxy_check": false,
 | 
			
		||||
    "timeout": 10000,
 | 
			
		||||
    "dodos_count": 1,
 | 
			
		||||
    "request_count": 1,
 | 
			
		||||
    "params": {},
 | 
			
		||||
    "headers": {},
 | 
			
		||||
    "cookies": {},
 | 
			
		||||
    "body": [],
 | 
			
		||||
    "proxies": [
 | 
			
		||||
        {
 | 
			
		||||
            "url": "http://example.com:8080",
 | 
			
		||||
            "username": "username",
 | 
			
		||||
            "password": "password"
 | 
			
		||||
        },
 | 
			
		||||
        {
 | 
			
		||||
            "url": "http://example.com:8080"
 | 
			
		||||
        }
 | 
			
		||||
    "yes": false,
 | 
			
		||||
    "timeout": "5s",
 | 
			
		||||
    "dodos": 8,
 | 
			
		||||
    "requests": 1000,
 | 
			
		||||
    "duration": "10s",
 | 
			
		||||
    "skip_verify": false,
 | 
			
		||||
 | 
			
		||||
    "params": [
 | 
			
		||||
        { "key1": ["value1", "value2", "value3", "value4"] },
 | 
			
		||||
        { "key1": "value" },
 | 
			
		||||
        { "key2": ["value1", "value2"] }
 | 
			
		||||
    ],
 | 
			
		||||
 | 
			
		||||
    "headers": [
 | 
			
		||||
        { "key1": ["value1", "value2", "value3", "value4"] },
 | 
			
		||||
        { "key1": "value" },
 | 
			
		||||
        { "key2": ["value1", "value2"] }
 | 
			
		||||
    ],
 | 
			
		||||
 | 
			
		||||
    "cookies": [
 | 
			
		||||
        { "key1": ["value1", "value2", "value3", "value4"] },
 | 
			
		||||
        { "key1": "value" },
 | 
			
		||||
        { "key2": ["value1", "value2"] }
 | 
			
		||||
    ],
 | 
			
		||||
 | 
			
		||||
    "body": ["body-text1", "body-text2", "body-text3"],
 | 
			
		||||
 | 
			
		||||
    "proxy": [
 | 
			
		||||
        "http://example.com:8080",
 | 
			
		||||
        "http://username:password@example.com:8080",
 | 
			
		||||
        "socks5://example.com:8080",
 | 
			
		||||
        "socks5h://example.com:8080"
 | 
			
		||||
    ]
 | 
			
		||||
}
 | 
			
		||||
 
 | 
			
		||||
							
								
								
									
										40
									
								
								config.yaml
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										40
									
								
								config.yaml
									
									
									
									
									
										Normal file
									
								
							@@ -0,0 +1,40 @@
 | 
			
		||||
method: "GET"
 | 
			
		||||
url: "https://example.com"
 | 
			
		||||
yes: false
 | 
			
		||||
timeout: "5s"
 | 
			
		||||
dodos: 8
 | 
			
		||||
requests: 1000
 | 
			
		||||
duration: "10s"
 | 
			
		||||
skip_verify: false
 | 
			
		||||
 | 
			
		||||
params:
 | 
			
		||||
    - key1: ["value1", "value2", "value3", "value4"]
 | 
			
		||||
    - key1: "value"
 | 
			
		||||
    - key2: ["value1", "value2"]
 | 
			
		||||
 | 
			
		||||
headers:
 | 
			
		||||
    - key1: ["value1", "value2", "value3", "value4"]
 | 
			
		||||
    - key1: "value"
 | 
			
		||||
    - key2: ["value1", "value2"]
 | 
			
		||||
 | 
			
		||||
cookies:
 | 
			
		||||
    - key1: ["value1", "value2", "value3", "value4"]
 | 
			
		||||
    - key1: "value"
 | 
			
		||||
    - key2: ["value1", "value2"]
 | 
			
		||||
 | 
			
		||||
# body: "body-text"
 | 
			
		||||
# OR
 | 
			
		||||
# A random body value will be selected from the list for each request
 | 
			
		||||
body:
 | 
			
		||||
    - "body-text1"
 | 
			
		||||
    - "body-text2"
 | 
			
		||||
    - "body-text3"
 | 
			
		||||
 | 
			
		||||
# proxy: "http://example.com:8080"
 | 
			
		||||
# OR
 | 
			
		||||
# A random proxy will be selected from the list for each request
 | 
			
		||||
proxy:
 | 
			
		||||
    - "http://example.com:8080"
 | 
			
		||||
    - "http://username:password@example.com:8080"
 | 
			
		||||
    - "socks5://example.com:8080"
 | 
			
		||||
    - "socks5h://example.com:8080"
 | 
			
		||||
							
								
								
									
										188
									
								
								config/cli.go
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										188
									
								
								config/cli.go
									
									
									
									
									
										Normal file
									
								
							@@ -0,0 +1,188 @@
 | 
			
		||||
package config
 | 
			
		||||
 | 
			
		||||
import (
 | 
			
		||||
	"flag"
 | 
			
		||||
	"fmt"
 | 
			
		||||
	"os"
 | 
			
		||||
	"strings"
 | 
			
		||||
	"time"
 | 
			
		||||
 | 
			
		||||
	"github.com/aykhans/dodo/types"
 | 
			
		||||
	"github.com/aykhans/dodo/utils"
 | 
			
		||||
)
 | 
			
		||||
 | 
			
		||||
const cliUsageText = `Usage:
 | 
			
		||||
  dodo [flags]
 | 
			
		||||
 | 
			
		||||
Examples:
 | 
			
		||||
 | 
			
		||||
Simple usage:
 | 
			
		||||
  dodo -u https://example.com -o 1m
 | 
			
		||||
 | 
			
		||||
Usage with config file:
 | 
			
		||||
  dodo -f /path/to/config/file/config.json
 | 
			
		||||
 | 
			
		||||
Usage with all flags:
 | 
			
		||||
  dodo -f /path/to/config/file/config.json \
 | 
			
		||||
    -u https://example.com -m POST \
 | 
			
		||||
    -d 10 -r 1000 -o 3m -t 3s \
 | 
			
		||||
    -b "body1" -body "body2" \
 | 
			
		||||
    -H "header1:value1" -header "header2:value2" \
 | 
			
		||||
    -p "param1=value1" -param "param2=value2" \
 | 
			
		||||
    -c "cookie1=value1" -cookie "cookie2=value2" \
 | 
			
		||||
    -x "http://proxy.example.com:8080" -proxy "socks5://proxy2.example.com:8080" \
 | 
			
		||||
    -skip-verify -y
 | 
			
		||||
 | 
			
		||||
Flags:
 | 
			
		||||
  -h, -help                   help for dodo
 | 
			
		||||
  -v, -version                version for dodo
 | 
			
		||||
  -y, -yes          bool      Answer yes to all questions (default %v)
 | 
			
		||||
  -f, -config-file  string    Path to the local config file or http(s) URL of the config file
 | 
			
		||||
  -d, -dodos        uint      Number of dodos(threads) (default %d)
 | 
			
		||||
  -r, -requests     uint      Number of total requests
 | 
			
		||||
  -o, -duration     Time      Maximum duration for the test (e.g. 30s, 1m, 5h)
 | 
			
		||||
  -t, -timeout      Time      Timeout for each request (e.g. 400ms, 15s, 1m10s) (default %v)
 | 
			
		||||
  -u, -url          string    URL for stress testing
 | 
			
		||||
  -m, -method       string    HTTP Method for the request (default %s)
 | 
			
		||||
  -b, -body         [string]  Body for the request (e.g. "body text")
 | 
			
		||||
  -p, -param        [string]  Parameter for the request (e.g. "key1=value1")
 | 
			
		||||
  -H, -header       [string]  Header for the request (e.g. "key1:value1")
 | 
			
		||||
  -c, -cookie       [string]  Cookie for the request (e.g. "key1=value1")
 | 
			
		||||
  -x, -proxy        [string]  Proxy for the request (e.g. "http://proxy.example.com:8080")
 | 
			
		||||
  -skip-verify      bool      Skip SSL/TLS certificate verification (default %v)`
 | 
			
		||||
 | 
			
		||||
func (config *Config) ReadCLI() (types.ConfigFile, error) {
 | 
			
		||||
	flag.Usage = func() {
 | 
			
		||||
		fmt.Printf(
 | 
			
		||||
			cliUsageText+"\n",
 | 
			
		||||
			DefaultYes,
 | 
			
		||||
			DefaultDodosCount,
 | 
			
		||||
			DefaultTimeout,
 | 
			
		||||
			DefaultMethod,
 | 
			
		||||
			DefaultSkipVerify,
 | 
			
		||||
		)
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	var (
 | 
			
		||||
		version      = false
 | 
			
		||||
		configFile   = ""
 | 
			
		||||
		yes          = false
 | 
			
		||||
		skipVerify   = false
 | 
			
		||||
		method       = ""
 | 
			
		||||
		url          types.RequestURL
 | 
			
		||||
		dodosCount   = uint(0)
 | 
			
		||||
		requestCount = uint(0)
 | 
			
		||||
		timeout      time.Duration
 | 
			
		||||
		duration     time.Duration
 | 
			
		||||
	)
 | 
			
		||||
 | 
			
		||||
	{
 | 
			
		||||
		flag.BoolVar(&version, "version", false, "Prints the version of the program")
 | 
			
		||||
		flag.BoolVar(&version, "v", false, "Prints the version of the program")
 | 
			
		||||
 | 
			
		||||
		flag.StringVar(&configFile, "config-file", "", "Path to the configuration file")
 | 
			
		||||
		flag.StringVar(&configFile, "f", "", "Path to the configuration file")
 | 
			
		||||
 | 
			
		||||
		flag.BoolVar(&yes, "yes", false, "Answer yes to all questions")
 | 
			
		||||
		flag.BoolVar(&yes, "y", false, "Answer yes to all questions")
 | 
			
		||||
 | 
			
		||||
		flag.BoolVar(&skipVerify, "skip-verify", false, "Skip SSL/TLS certificate verification")
 | 
			
		||||
 | 
			
		||||
		flag.StringVar(&method, "method", "", "HTTP Method")
 | 
			
		||||
		flag.StringVar(&method, "m", "", "HTTP Method")
 | 
			
		||||
 | 
			
		||||
		flag.Var(&url, "url", "URL to send the request")
 | 
			
		||||
		flag.Var(&url, "u", "URL to send the request")
 | 
			
		||||
 | 
			
		||||
		flag.UintVar(&dodosCount, "dodos", 0, "Number of dodos(threads)")
 | 
			
		||||
		flag.UintVar(&dodosCount, "d", 0, "Number of dodos(threads)")
 | 
			
		||||
 | 
			
		||||
		flag.UintVar(&requestCount, "requests", 0, "Number of total requests")
 | 
			
		||||
		flag.UintVar(&requestCount, "r", 0, "Number of total requests")
 | 
			
		||||
 | 
			
		||||
		flag.DurationVar(&duration, "duration", 0, "Maximum duration of the test")
 | 
			
		||||
		flag.DurationVar(&duration, "o", 0, "Maximum duration of the test")
 | 
			
		||||
 | 
			
		||||
		flag.DurationVar(&timeout, "timeout", 0, "Timeout for each request (e.g. 400ms, 15s, 1m10s)")
 | 
			
		||||
		flag.DurationVar(&timeout, "t", 0, "Timeout for each request (e.g. 400ms, 15s, 1m10s)")
 | 
			
		||||
 | 
			
		||||
		flag.Var(&config.Params, "param", "URL parameter to send with the request")
 | 
			
		||||
		flag.Var(&config.Params, "p", "URL parameter to send with the request")
 | 
			
		||||
 | 
			
		||||
		flag.Var(&config.Headers, "header", "Header to send with the request")
 | 
			
		||||
		flag.Var(&config.Headers, "H", "Header to send with the request")
 | 
			
		||||
 | 
			
		||||
		flag.Var(&config.Cookies, "cookie", "Cookie to send with the request")
 | 
			
		||||
		flag.Var(&config.Cookies, "c", "Cookie to send with the request")
 | 
			
		||||
 | 
			
		||||
		flag.Var(&config.Body, "body", "Body to send with the request")
 | 
			
		||||
		flag.Var(&config.Body, "b", "Body to send with the request")
 | 
			
		||||
 | 
			
		||||
		flag.Var(&config.Proxies, "proxy", "Proxy to use for the request")
 | 
			
		||||
		flag.Var(&config.Proxies, "x", "Proxy to use for the request")
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	flag.Parse()
 | 
			
		||||
 | 
			
		||||
	if len(os.Args) <= 1 {
 | 
			
		||||
		flag.CommandLine.Usage()
 | 
			
		||||
		os.Exit(0)
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	if args := flag.Args(); len(args) > 0 {
 | 
			
		||||
		return types.ConfigFile(configFile), fmt.Errorf("unexpected arguments: %v", strings.Join(args, ", "))
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	if version {
 | 
			
		||||
		fmt.Printf("dodo version %s\n", VERSION)
 | 
			
		||||
		os.Exit(0)
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	flag.Visit(func(f *flag.Flag) {
 | 
			
		||||
		switch f.Name {
 | 
			
		||||
		case "method", "m":
 | 
			
		||||
			config.Method = utils.ToPtr(method)
 | 
			
		||||
		case "url", "u":
 | 
			
		||||
			config.URL = utils.ToPtr(url)
 | 
			
		||||
		case "dodos", "d":
 | 
			
		||||
			config.DodosCount = utils.ToPtr(dodosCount)
 | 
			
		||||
		case "requests", "r":
 | 
			
		||||
			config.RequestCount = utils.ToPtr(requestCount)
 | 
			
		||||
		case "duration", "o":
 | 
			
		||||
			config.Duration = &types.Duration{Duration: duration}
 | 
			
		||||
		case "timeout", "t":
 | 
			
		||||
			config.Timeout = &types.Timeout{Duration: timeout}
 | 
			
		||||
		case "yes", "y":
 | 
			
		||||
			config.Yes = utils.ToPtr(yes)
 | 
			
		||||
		case "skip-verify":
 | 
			
		||||
			config.SkipVerify = utils.ToPtr(skipVerify)
 | 
			
		||||
		}
 | 
			
		||||
	})
 | 
			
		||||
 | 
			
		||||
	return types.ConfigFile(configFile), nil
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
// CLIYesOrNoReader reads a yes or no answer from the command line.
 | 
			
		||||
// It prompts the user with the given message and default value,
 | 
			
		||||
// and returns true if the user answers "y" or "Y", and false otherwise.
 | 
			
		||||
// If there is an error while reading the input, it returns false.
 | 
			
		||||
// If the user simply presses enter without providing any input,
 | 
			
		||||
// it returns the default value specified by the `dft` parameter.
 | 
			
		||||
func CLIYesOrNoReader(message string, dft bool) bool {
 | 
			
		||||
	var answer string
 | 
			
		||||
	defaultMessage := "Y/n"
 | 
			
		||||
	if !dft {
 | 
			
		||||
		defaultMessage = "y/N"
 | 
			
		||||
	}
 | 
			
		||||
	fmt.Printf("%s [%s]: ", message, defaultMessage)
 | 
			
		||||
	if _, err := fmt.Scanln(&answer); err != nil {
 | 
			
		||||
		if err.Error() == "unexpected newline" {
 | 
			
		||||
			return dft
 | 
			
		||||
		}
 | 
			
		||||
		return false
 | 
			
		||||
	}
 | 
			
		||||
	if answer == "" {
 | 
			
		||||
		return dft
 | 
			
		||||
	}
 | 
			
		||||
	return answer == "y" || answer == "Y"
 | 
			
		||||
}
 | 
			
		||||
							
								
								
									
										413
									
								
								config/config.go
									
									
									
									
									
								
							
							
						
						
									
										413
									
								
								config/config.go
									
									
									
									
									
								
							@@ -1,43 +1,85 @@
 | 
			
		||||
package config
 | 
			
		||||
 | 
			
		||||
import (
 | 
			
		||||
	"bytes"
 | 
			
		||||
	"errors"
 | 
			
		||||
	"fmt"
 | 
			
		||||
	"math/rand"
 | 
			
		||||
	"net/url"
 | 
			
		||||
	"os"
 | 
			
		||||
	"slices"
 | 
			
		||||
	"strings"
 | 
			
		||||
	"text/template"
 | 
			
		||||
	"time"
 | 
			
		||||
 | 
			
		||||
	. "github.com/aykhans/dodo/types"
 | 
			
		||||
	"github.com/aykhans/dodo/types"
 | 
			
		||||
	"github.com/aykhans/dodo/utils"
 | 
			
		||||
	"github.com/jedib0t/go-pretty/v6/table"
 | 
			
		||||
)
 | 
			
		||||
 | 
			
		||||
const (
 | 
			
		||||
	VERSION                 string = "0.5.4"
 | 
			
		||||
	VERSION             string        = "0.7.3"
 | 
			
		||||
	DefaultUserAgent    string        = "Dodo/" + VERSION
 | 
			
		||||
	ProxyCheckURL           string = "https://www.google.com"
 | 
			
		||||
	DefaultMethod       string        = "GET"
 | 
			
		||||
	DefaultTimeout          uint32 = 10000 // Milliseconds (10 seconds)
 | 
			
		||||
	DefaultTimeout      time.Duration = time.Second * 10
 | 
			
		||||
	DefaultDodosCount   uint          = 1
 | 
			
		||||
	DefaultRequestCount     uint   = 1
 | 
			
		||||
	MaxDodosCountForProxies uint   = 20 // Max dodos count for proxy check
 | 
			
		||||
	DefaultRequestCount uint          = 0
 | 
			
		||||
	DefaultDuration     time.Duration = 0
 | 
			
		||||
	DefaultYes          bool          = false
 | 
			
		||||
	DefaultSkipVerify   bool          = false
 | 
			
		||||
)
 | 
			
		||||
 | 
			
		||||
var SupportedProxySchemes []string = []string{"http", "socks5", "socks5h"}
 | 
			
		||||
 | 
			
		||||
type RequestConfig struct {
 | 
			
		||||
	Method       string
 | 
			
		||||
	URL          *url.URL
 | 
			
		||||
	URL          url.URL
 | 
			
		||||
	Timeout      time.Duration
 | 
			
		||||
	DodosCount   uint
 | 
			
		||||
	RequestCount uint
 | 
			
		||||
	Params       map[string][]string
 | 
			
		||||
	Headers      map[string][]string
 | 
			
		||||
	Cookies      map[string][]string
 | 
			
		||||
	Proxies      []Proxy
 | 
			
		||||
	Body         []string
 | 
			
		||||
	Duration     time.Duration
 | 
			
		||||
	Yes          bool
 | 
			
		||||
	NoProxyCheck bool
 | 
			
		||||
	SkipVerify   bool
 | 
			
		||||
	Params       types.Params
 | 
			
		||||
	Headers      types.Headers
 | 
			
		||||
	Cookies      types.Cookies
 | 
			
		||||
	Body         types.Body
 | 
			
		||||
	Proxies      types.Proxies
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
func (config *RequestConfig) Print() {
 | 
			
		||||
func NewRequestConfig(conf *Config) *RequestConfig {
 | 
			
		||||
	return &RequestConfig{
 | 
			
		||||
		Method:       *conf.Method,
 | 
			
		||||
		URL:          conf.URL.URL,
 | 
			
		||||
		Timeout:      conf.Timeout.Duration,
 | 
			
		||||
		DodosCount:   *conf.DodosCount,
 | 
			
		||||
		RequestCount: *conf.RequestCount,
 | 
			
		||||
		Duration:     conf.Duration.Duration,
 | 
			
		||||
		Yes:          *conf.Yes,
 | 
			
		||||
		SkipVerify:   *conf.SkipVerify,
 | 
			
		||||
		Params:       conf.Params,
 | 
			
		||||
		Headers:      conf.Headers,
 | 
			
		||||
		Cookies:      conf.Cookies,
 | 
			
		||||
		Body:         conf.Body,
 | 
			
		||||
		Proxies:      conf.Proxies,
 | 
			
		||||
	}
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
func (rc *RequestConfig) GetValidDodosCountForRequests() uint {
 | 
			
		||||
	if rc.RequestCount == 0 {
 | 
			
		||||
		return rc.DodosCount
 | 
			
		||||
	}
 | 
			
		||||
	return min(rc.DodosCount, rc.RequestCount)
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
func (rc *RequestConfig) GetMaxConns(minConns uint) uint {
 | 
			
		||||
	maxConns := max(
 | 
			
		||||
		minConns, rc.GetValidDodosCountForRequests(),
 | 
			
		||||
	)
 | 
			
		||||
	return ((maxConns * 50 / 100) + maxConns)
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
func (rc *RequestConfig) Print() {
 | 
			
		||||
	t := table.NewWriter()
 | 
			
		||||
	t.SetOutputMirror(os.Stdout)
 | 
			
		||||
	t.SetStyle(table.StyleLight)
 | 
			
		||||
@@ -56,151 +98,229 @@ func (config *RequestConfig) Print() {
 | 
			
		||||
			WidthMax: 50},
 | 
			
		||||
	})
 | 
			
		||||
 | 
			
		||||
	newHeaders := make(map[string][]string)
 | 
			
		||||
	newHeaders["User-Agent"] = []string{DefaultUserAgent}
 | 
			
		||||
	for k, v := range config.Headers {
 | 
			
		||||
		newHeaders[k] = v
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	t.AppendHeader(table.Row{"Request Configuration"})
 | 
			
		||||
	t.AppendRow(table.Row{"Method", config.Method})
 | 
			
		||||
	t.AppendRow(table.Row{"URL", rc.URL.String()})
 | 
			
		||||
	t.AppendSeparator()
 | 
			
		||||
	t.AppendRow(table.Row{"URL", config.URL})
 | 
			
		||||
	t.AppendRow(table.Row{"Method", rc.Method})
 | 
			
		||||
	t.AppendSeparator()
 | 
			
		||||
	t.AppendRow(table.Row{"Timeout", config.Timeout})
 | 
			
		||||
	t.AppendRow(table.Row{"Timeout", rc.Timeout})
 | 
			
		||||
	t.AppendSeparator()
 | 
			
		||||
	t.AppendRow(table.Row{"Dodos", config.DodosCount})
 | 
			
		||||
	t.AppendRow(table.Row{"Dodos", rc.DodosCount})
 | 
			
		||||
	t.AppendSeparator()
 | 
			
		||||
	t.AppendRow(table.Row{"Requests", config.RequestCount})
 | 
			
		||||
	if rc.RequestCount > 0 {
 | 
			
		||||
		t.AppendRow(table.Row{"Requests", rc.RequestCount})
 | 
			
		||||
	} else {
 | 
			
		||||
		t.AppendRow(table.Row{"Requests"})
 | 
			
		||||
	}
 | 
			
		||||
	t.AppendSeparator()
 | 
			
		||||
	t.AppendRow(table.Row{"Params", string(utils.PrettyJSONMarshal(config.Params, 3, "", "  "))})
 | 
			
		||||
	if rc.Duration > 0 {
 | 
			
		||||
		t.AppendRow(table.Row{"Duration", rc.Duration})
 | 
			
		||||
	} else {
 | 
			
		||||
		t.AppendRow(table.Row{"Duration"})
 | 
			
		||||
	}
 | 
			
		||||
	t.AppendSeparator()
 | 
			
		||||
	t.AppendRow(table.Row{"Headers", string(utils.PrettyJSONMarshal(newHeaders, 3, "", "  "))})
 | 
			
		||||
	t.AppendRow(table.Row{"Params", rc.Params.String()})
 | 
			
		||||
	t.AppendSeparator()
 | 
			
		||||
	t.AppendRow(table.Row{"Cookies", string(utils.PrettyJSONMarshal(config.Cookies, 3, "", "  "))})
 | 
			
		||||
	t.AppendRow(table.Row{"Headers", rc.Headers.String()})
 | 
			
		||||
	t.AppendSeparator()
 | 
			
		||||
	t.AppendRow(table.Row{"Proxies Count", string(utils.PrettyJSONMarshal(config.Proxies, 3, "", "  "))})
 | 
			
		||||
	t.AppendRow(table.Row{"Cookies", rc.Cookies.String()})
 | 
			
		||||
	t.AppendSeparator()
 | 
			
		||||
	t.AppendRow(table.Row{"Proxy Check", !config.NoProxyCheck})
 | 
			
		||||
	t.AppendRow(table.Row{"Proxy", rc.Proxies.String()})
 | 
			
		||||
	t.AppendSeparator()
 | 
			
		||||
	t.AppendRow(table.Row{"Body", string(utils.PrettyJSONMarshal(config.Body, 3, "", "  "))})
 | 
			
		||||
	t.AppendRow(table.Row{"Body", rc.Body.String()})
 | 
			
		||||
	t.AppendSeparator()
 | 
			
		||||
	t.AppendRow(table.Row{"Skip Verify", rc.SkipVerify})
 | 
			
		||||
 | 
			
		||||
	t.Render()
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
func (config *RequestConfig) GetValidDodosCountForRequests() uint {
 | 
			
		||||
	return min(config.DodosCount, config.RequestCount)
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
func (config *RequestConfig) GetValidDodosCountForProxies() uint {
 | 
			
		||||
	return min(config.DodosCount, uint(len(config.Proxies)), MaxDodosCountForProxies)
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
func (config *RequestConfig) GetMaxConns(minConns uint) uint {
 | 
			
		||||
	maxConns := max(
 | 
			
		||||
		minConns, uint(config.GetValidDodosCountForRequests()),
 | 
			
		||||
	)
 | 
			
		||||
	return ((maxConns * 50 / 100) + maxConns)
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
type Config struct {
 | 
			
		||||
	Method       string       `json:"method" validate:"http_method"` // custom validations: http_method
 | 
			
		||||
	URL          string       `json:"url" validate:"http_url,required"`
 | 
			
		||||
	Timeout      uint32       `json:"timeout" validate:"gte=1,lte=100000"`
 | 
			
		||||
	DodosCount   uint         `json:"dodos_count" validate:"gte=1"`
 | 
			
		||||
	RequestCount uint         `json:"request_count" validation_name:"request-count" validate:"gte=1"`
 | 
			
		||||
	NoProxyCheck Option[bool] `json:"no_proxy_check"`
 | 
			
		||||
	Method       *string           `json:"method" yaml:"method"`
 | 
			
		||||
	URL          *types.RequestURL `json:"url" yaml:"url"`
 | 
			
		||||
	Timeout      *types.Timeout    `json:"timeout" yaml:"timeout"`
 | 
			
		||||
	DodosCount   *uint             `json:"dodos" yaml:"dodos"`
 | 
			
		||||
	RequestCount *uint             `json:"requests" yaml:"requests"`
 | 
			
		||||
	Duration     *types.Duration   `json:"duration" yaml:"duration"`
 | 
			
		||||
	Yes          *bool             `json:"yes" yaml:"yes"`
 | 
			
		||||
	SkipVerify   *bool             `json:"skip_verify" yaml:"skip_verify"`
 | 
			
		||||
	Params       types.Params      `json:"params" yaml:"params"`
 | 
			
		||||
	Headers      types.Headers     `json:"headers" yaml:"headers"`
 | 
			
		||||
	Cookies      types.Cookies     `json:"cookies" yaml:"cookies"`
 | 
			
		||||
	Body         types.Body        `json:"body" yaml:"body"`
 | 
			
		||||
	Proxies      types.Proxies     `json:"proxy" yaml:"proxy"`
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
func NewConfig(
 | 
			
		||||
	method string,
 | 
			
		||||
	timeout uint32,
 | 
			
		||||
	dodosCount uint,
 | 
			
		||||
	requestCount uint,
 | 
			
		||||
	noProxyCheck Option[bool],
 | 
			
		||||
) *Config {
 | 
			
		||||
	if noProxyCheck == nil {
 | 
			
		||||
		noProxyCheck = NewNoneOption[bool]()
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	return &Config{
 | 
			
		||||
		Method:       method,
 | 
			
		||||
		Timeout:      timeout,
 | 
			
		||||
		DodosCount:   dodosCount,
 | 
			
		||||
		RequestCount: requestCount,
 | 
			
		||||
		NoProxyCheck: noProxyCheck,
 | 
			
		||||
	}
 | 
			
		||||
func NewConfig() *Config {
 | 
			
		||||
	return &Config{}
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
func (config *Config) MergeConfigs(newConfig *Config) {
 | 
			
		||||
	if newConfig.Method != "" {
 | 
			
		||||
func (config *Config) Validate() []error {
 | 
			
		||||
	var errs []error
 | 
			
		||||
	if utils.IsNilOrZero(config.URL) {
 | 
			
		||||
		errs = append(errs, errors.New("request URL is required"))
 | 
			
		||||
	} else {
 | 
			
		||||
		if config.URL.Scheme != "http" && config.URL.Scheme != "https" {
 | 
			
		||||
			errs = append(errs, errors.New("request URL scheme must be http or https"))
 | 
			
		||||
		}
 | 
			
		||||
 | 
			
		||||
		urlParams := types.Params{}
 | 
			
		||||
		for key, values := range config.URL.Query() {
 | 
			
		||||
			for _, value := range values {
 | 
			
		||||
				urlParams = append(urlParams, types.KeyValue[string, []string]{
 | 
			
		||||
					Key:   key,
 | 
			
		||||
					Value: []string{value},
 | 
			
		||||
				})
 | 
			
		||||
			}
 | 
			
		||||
		}
 | 
			
		||||
		config.Params = append(urlParams, config.Params...)
 | 
			
		||||
		config.URL.RawQuery = ""
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	if utils.IsNilOrZero(config.Method) {
 | 
			
		||||
		errs = append(errs, errors.New("request method is required"))
 | 
			
		||||
	}
 | 
			
		||||
	if utils.IsNilOrZero(config.Timeout) {
 | 
			
		||||
		errs = append(errs, errors.New("request timeout must be greater than 0"))
 | 
			
		||||
	}
 | 
			
		||||
	if utils.IsNilOrZero(config.DodosCount) {
 | 
			
		||||
		errs = append(errs, errors.New("dodos count must be greater than 0"))
 | 
			
		||||
	}
 | 
			
		||||
	if utils.IsNilOrZero(config.Duration) && utils.IsNilOrZero(config.RequestCount) {
 | 
			
		||||
		errs = append(errs, errors.New("you should provide at least one of duration or request count"))
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	for i, proxy := range config.Proxies {
 | 
			
		||||
		if proxy.String() == "" {
 | 
			
		||||
			errs = append(errs, fmt.Errorf("proxies[%d]: proxy cannot be empty", i))
 | 
			
		||||
		} else if schema := proxy.Scheme; !slices.Contains(SupportedProxySchemes, schema) {
 | 
			
		||||
			errs = append(errs,
 | 
			
		||||
				fmt.Errorf("proxies[%d]: proxy has unsupported scheme \"%s\" (supported schemes: %s)",
 | 
			
		||||
					i, proxy.String(), strings.Join(SupportedProxySchemes, ", "),
 | 
			
		||||
				),
 | 
			
		||||
			)
 | 
			
		||||
		}
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	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
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
func (config *Config) MergeConfig(newConfig *Config) {
 | 
			
		||||
	if newConfig.Method != nil {
 | 
			
		||||
		config.Method = newConfig.Method
 | 
			
		||||
	}
 | 
			
		||||
	if newConfig.URL != "" {
 | 
			
		||||
	if newConfig.URL != nil {
 | 
			
		||||
		config.URL = newConfig.URL
 | 
			
		||||
	}
 | 
			
		||||
	if newConfig.Timeout != 0 {
 | 
			
		||||
	if newConfig.Timeout != nil {
 | 
			
		||||
		config.Timeout = newConfig.Timeout
 | 
			
		||||
	}
 | 
			
		||||
	if newConfig.DodosCount != 0 {
 | 
			
		||||
	if newConfig.DodosCount != nil {
 | 
			
		||||
		config.DodosCount = newConfig.DodosCount
 | 
			
		||||
	}
 | 
			
		||||
	if newConfig.RequestCount != 0 {
 | 
			
		||||
	if newConfig.RequestCount != nil {
 | 
			
		||||
		config.RequestCount = newConfig.RequestCount
 | 
			
		||||
	}
 | 
			
		||||
	if !newConfig.NoProxyCheck.IsNone() {
 | 
			
		||||
		config.NoProxyCheck = newConfig.NoProxyCheck
 | 
			
		||||
	if newConfig.Duration != nil {
 | 
			
		||||
		config.Duration = newConfig.Duration
 | 
			
		||||
	}
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
func (config *Config) SetDefaults() {
 | 
			
		||||
	if config.Method == "" {
 | 
			
		||||
		config.Method = DefaultMethod
 | 
			
		||||
	if newConfig.Yes != nil {
 | 
			
		||||
		config.Yes = newConfig.Yes
 | 
			
		||||
	}
 | 
			
		||||
	if config.Timeout == 0 {
 | 
			
		||||
		config.Timeout = DefaultTimeout
 | 
			
		||||
	if newConfig.SkipVerify != nil {
 | 
			
		||||
		config.SkipVerify = newConfig.SkipVerify
 | 
			
		||||
	}
 | 
			
		||||
	if config.DodosCount == 0 {
 | 
			
		||||
		config.DodosCount = DefaultDodosCount
 | 
			
		||||
	}
 | 
			
		||||
	if config.RequestCount == 0 {
 | 
			
		||||
		config.RequestCount = DefaultRequestCount
 | 
			
		||||
	}
 | 
			
		||||
	if config.NoProxyCheck.IsNone() {
 | 
			
		||||
		config.NoProxyCheck = NewOption(false)
 | 
			
		||||
	}
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
type Proxy struct {
 | 
			
		||||
	URL      string `json:"url" validate:"required,proxy_url"`
 | 
			
		||||
	Username string `json:"username"`
 | 
			
		||||
	Password string `json:"password"`
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
type JSONConfig struct {
 | 
			
		||||
	*Config
 | 
			
		||||
	Params  map[string][]string `json:"params"`
 | 
			
		||||
	Headers map[string][]string `json:"headers"`
 | 
			
		||||
	Cookies map[string][]string `json:"cookies"`
 | 
			
		||||
	Proxies []Proxy             `json:"proxies" validate:"dive"`
 | 
			
		||||
	Body    []string            `json:"body"`
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
func NewJSONConfig(
 | 
			
		||||
	config *Config,
 | 
			
		||||
	params map[string][]string,
 | 
			
		||||
	headers map[string][]string,
 | 
			
		||||
	cookies map[string][]string,
 | 
			
		||||
	proxies []Proxy,
 | 
			
		||||
	body []string,
 | 
			
		||||
) *JSONConfig {
 | 
			
		||||
	return &JSONConfig{
 | 
			
		||||
		config, params, headers, cookies, proxies, body,
 | 
			
		||||
	}
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
func (config *JSONConfig) MergeConfigs(newConfig *JSONConfig) {
 | 
			
		||||
	config.Config.MergeConfigs(newConfig.Config)
 | 
			
		||||
	if len(newConfig.Params) != 0 {
 | 
			
		||||
		config.Params = newConfig.Params
 | 
			
		||||
	}
 | 
			
		||||
@@ -218,28 +338,27 @@ func (config *JSONConfig) MergeConfigs(newConfig *JSONConfig) {
 | 
			
		||||
	}
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
type CLIConfig struct {
 | 
			
		||||
	*Config
 | 
			
		||||
	Yes        Option[bool] `json:"yes" validate:"omitempty"`
 | 
			
		||||
	ConfigFile string       `validation_name:"config-file" validate:"omitempty,filepath"`
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
func NewCLIConfig(
 | 
			
		||||
	config *Config,
 | 
			
		||||
	yes Option[bool],
 | 
			
		||||
	configFile string,
 | 
			
		||||
) *CLIConfig {
 | 
			
		||||
	return &CLIConfig{
 | 
			
		||||
		config, yes, configFile,
 | 
			
		||||
func (config *Config) SetDefaults() {
 | 
			
		||||
	if config.Method == nil {
 | 
			
		||||
		config.Method = utils.ToPtr(DefaultMethod)
 | 
			
		||||
	}
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
func (config *CLIConfig) MergeConfigs(newConfig *CLIConfig) {
 | 
			
		||||
	config.Config.MergeConfigs(newConfig.Config)
 | 
			
		||||
	if newConfig.ConfigFile != "" {
 | 
			
		||||
		config.ConfigFile = newConfig.ConfigFile
 | 
			
		||||
	if config.Timeout == nil {
 | 
			
		||||
		config.Timeout = &types.Timeout{Duration: DefaultTimeout}
 | 
			
		||||
	}
 | 
			
		||||
	if !newConfig.Yes.IsNone() {
 | 
			
		||||
		config.Yes = newConfig.Yes
 | 
			
		||||
	if config.DodosCount == nil {
 | 
			
		||||
		config.DodosCount = utils.ToPtr(DefaultDodosCount)
 | 
			
		||||
	}
 | 
			
		||||
	if config.RequestCount == nil {
 | 
			
		||||
		config.RequestCount = utils.ToPtr(DefaultRequestCount)
 | 
			
		||||
	}
 | 
			
		||||
	if config.Duration == nil {
 | 
			
		||||
		config.Duration = &types.Duration{Duration: DefaultDuration}
 | 
			
		||||
	}
 | 
			
		||||
	if config.Yes == nil {
 | 
			
		||||
		config.Yes = utils.ToPtr(DefaultYes)
 | 
			
		||||
	}
 | 
			
		||||
	if config.SkipVerify == nil {
 | 
			
		||||
		config.SkipVerify = utils.ToPtr(DefaultSkipVerify)
 | 
			
		||||
	}
 | 
			
		||||
	config.Headers.SetIfNotExists("User-Agent", DefaultUserAgent)
 | 
			
		||||
}
 | 
			
		||||
 
 | 
			
		||||
							
								
								
									
										84
									
								
								config/file.go
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										84
									
								
								config/file.go
									
									
									
									
									
										Normal file
									
								
							@@ -0,0 +1,84 @@
 | 
			
		||||
package config
 | 
			
		||||
 | 
			
		||||
import (
 | 
			
		||||
	"encoding/json"
 | 
			
		||||
	"errors"
 | 
			
		||||
	"fmt"
 | 
			
		||||
	"io"
 | 
			
		||||
	"net/http"
 | 
			
		||||
	"os"
 | 
			
		||||
	"slices"
 | 
			
		||||
	"strings"
 | 
			
		||||
	"time"
 | 
			
		||||
 | 
			
		||||
	"github.com/aykhans/dodo/types"
 | 
			
		||||
	"gopkg.in/yaml.v3"
 | 
			
		||||
)
 | 
			
		||||
 | 
			
		||||
var supportedFileTypes = []string{"json", "yaml", "yml"}
 | 
			
		||||
 | 
			
		||||
func (config *Config) ReadFile(filePath types.ConfigFile) error {
 | 
			
		||||
	var (
 | 
			
		||||
		data []byte
 | 
			
		||||
		err  error
 | 
			
		||||
	)
 | 
			
		||||
 | 
			
		||||
	fileExt := filePath.Extension()
 | 
			
		||||
	if slices.Contains(supportedFileTypes, fileExt) {
 | 
			
		||||
		if filePath.LocationType() == types.FileLocationTypeRemoteHTTP {
 | 
			
		||||
			client := &http.Client{
 | 
			
		||||
				Timeout: 10 * time.Second,
 | 
			
		||||
			}
 | 
			
		||||
 | 
			
		||||
			resp, err := client.Get(filePath.String())
 | 
			
		||||
			if err != nil {
 | 
			
		||||
				return fmt.Errorf("failed to fetch config file from %s", filePath)
 | 
			
		||||
			}
 | 
			
		||||
			defer func() { _ = resp.Body.Close() }()
 | 
			
		||||
 | 
			
		||||
			data, err = io.ReadAll(io.Reader(resp.Body))
 | 
			
		||||
			if err != nil {
 | 
			
		||||
				return fmt.Errorf("failed to read config file from %s", filePath)
 | 
			
		||||
			}
 | 
			
		||||
		} else {
 | 
			
		||||
			data, err = os.ReadFile(filePath.String())
 | 
			
		||||
			if err != nil {
 | 
			
		||||
				return errors.New("failed to read config file from " + filePath.String())
 | 
			
		||||
			}
 | 
			
		||||
		}
 | 
			
		||||
 | 
			
		||||
		switch fileExt {
 | 
			
		||||
		case "json":
 | 
			
		||||
			return parseJSONConfig(data, config)
 | 
			
		||||
		case "yml", "yaml":
 | 
			
		||||
			return parseYAMLConfig(data, config)
 | 
			
		||||
		}
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	return fmt.Errorf("unsupported config file type (supported types: %v)", strings.Join(supportedFileTypes, ", "))
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
func parseJSONConfig(data []byte, config *Config) error {
 | 
			
		||||
	err := json.Unmarshal(data, &config)
 | 
			
		||||
	if err != nil {
 | 
			
		||||
		switch parsedErr := err.(type) {
 | 
			
		||||
		case *json.SyntaxError:
 | 
			
		||||
			return fmt.Errorf("JSON Config file: invalid syntax at byte offset %d", parsedErr.Offset)
 | 
			
		||||
		case *json.UnmarshalTypeError:
 | 
			
		||||
			return fmt.Errorf("JSON Config file: invalid type %v for field %s, expected %v", parsedErr.Value, parsedErr.Field, parsedErr.Type)
 | 
			
		||||
		default:
 | 
			
		||||
			return fmt.Errorf("JSON Config file: %s", err.Error())
 | 
			
		||||
		}
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	return nil
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
func parseYAMLConfig(data []byte, config *Config) error {
 | 
			
		||||
	err := yaml.Unmarshal(data, &config)
 | 
			
		||||
	if err != nil {
 | 
			
		||||
		return fmt.Errorf("YAML Config file: %s", err.Error())
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	return nil
 | 
			
		||||
}
 | 
			
		||||
@@ -1,117 +0,0 @@
 | 
			
		||||
package customerrors
 | 
			
		||||
 | 
			
		||||
import (
 | 
			
		||||
	"errors"
 | 
			
		||||
	"fmt"
 | 
			
		||||
 | 
			
		||||
	"github.com/go-playground/validator/v10"
 | 
			
		||||
)
 | 
			
		||||
 | 
			
		||||
var (
 | 
			
		||||
	ErrInvalidJSON = errors.New("invalid JSON file")
 | 
			
		||||
	ErrInvalidFile = errors.New("invalid file")
 | 
			
		||||
	ErrInterrupt   = errors.New("interrupted")
 | 
			
		||||
	ErrNoInternet  = errors.New("no internet connection")
 | 
			
		||||
	ErrTimeout     = errors.New("timeout")
 | 
			
		||||
)
 | 
			
		||||
 | 
			
		||||
func As(err error, target any) bool {
 | 
			
		||||
	return errors.As(err, target)
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
func Is(err, target error) bool {
 | 
			
		||||
	return errors.Is(err, target)
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
type Error interface {
 | 
			
		||||
	Error() string
 | 
			
		||||
	Unwrap() error
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
type TypeError struct {
 | 
			
		||||
	Expected string
 | 
			
		||||
	Received string
 | 
			
		||||
	Field    string
 | 
			
		||||
	err      error
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
func NewTypeError(expected, received, field string, err error) *TypeError {
 | 
			
		||||
	return &TypeError{
 | 
			
		||||
		Expected: expected,
 | 
			
		||||
		Received: received,
 | 
			
		||||
		Field:    field,
 | 
			
		||||
		err:      err,
 | 
			
		||||
	}
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
func (e *TypeError) Error() string {
 | 
			
		||||
	return "Expected " + e.Expected + " but received " + e.Received + " in field " + e.Field
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
func (e *TypeError) Unwrap() error {
 | 
			
		||||
	return e.err
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
type InvalidFileError struct {
 | 
			
		||||
	FileName string
 | 
			
		||||
	err      error
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
func NewInvalidFileError(fileName string, err error) *InvalidFileError {
 | 
			
		||||
	return &InvalidFileError{
 | 
			
		||||
		FileName: fileName,
 | 
			
		||||
		err:      err,
 | 
			
		||||
	}
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
func (e *InvalidFileError) Error() string {
 | 
			
		||||
	return "Invalid file: " + e.FileName
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
func (e *InvalidFileError) Unwrap() error {
 | 
			
		||||
	return e.err
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
type FileNotFoundError struct {
 | 
			
		||||
	FileName string
 | 
			
		||||
	err      error
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
func NewFileNotFoundError(fileName string, err error) *FileNotFoundError {
 | 
			
		||||
	return &FileNotFoundError{
 | 
			
		||||
		FileName: fileName,
 | 
			
		||||
		err:      err,
 | 
			
		||||
	}
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
func (e *FileNotFoundError) Error() string {
 | 
			
		||||
	return "File not found: " + e.FileName
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
func (e *FileNotFoundError) Unwrap() error {
 | 
			
		||||
	return e.err
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
type ValidationErrors struct {
 | 
			
		||||
	MapErrors map[string]string
 | 
			
		||||
	errors    validator.ValidationErrors
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
func NewValidationErrors(errsMap map[string]string, errs validator.ValidationErrors) *ValidationErrors {
 | 
			
		||||
	return &ValidationErrors{
 | 
			
		||||
		MapErrors: errsMap,
 | 
			
		||||
		errors:    errs,
 | 
			
		||||
	}
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
func (errs *ValidationErrors) Error() string {
 | 
			
		||||
	var errorsStr string
 | 
			
		||||
	for k, v := range errs.MapErrors {
 | 
			
		||||
		errorsStr += fmt.Sprintf("[%s]: %s\n", k, v)
 | 
			
		||||
	}
 | 
			
		||||
	return errorsStr
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
func (errs *ValidationErrors) Unwrap() error {
 | 
			
		||||
	return errs.errors
 | 
			
		||||
}
 | 
			
		||||
@@ -1,68 +0,0 @@
 | 
			
		||||
package customerrors
 | 
			
		||||
 | 
			
		||||
import (
 | 
			
		||||
	"fmt"
 | 
			
		||||
	"net"
 | 
			
		||||
	"net/url"
 | 
			
		||||
	"strings"
 | 
			
		||||
 | 
			
		||||
	"github.com/go-playground/validator/v10"
 | 
			
		||||
)
 | 
			
		||||
 | 
			
		||||
func OSErrorFormater(err error) error {
 | 
			
		||||
	errStr := err.Error()
 | 
			
		||||
	if strings.Contains(errStr, "no such file or directory") {
 | 
			
		||||
		fileName1 := strings.Index(errStr, "open")
 | 
			
		||||
		fileName2 := strings.LastIndex(errStr, ":")
 | 
			
		||||
		return NewFileNotFoundError(errStr[fileName1+5:fileName2], err)
 | 
			
		||||
	}
 | 
			
		||||
	return ErrInvalidFile
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
func shortenNamespace(namespace string) string {
 | 
			
		||||
	return namespace[strings.Index(namespace, ".")+1:]
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
func ValidationErrorsFormater(errs validator.ValidationErrors) error {
 | 
			
		||||
	errsStr := make(map[string]string)
 | 
			
		||||
	for _, err := range errs {
 | 
			
		||||
		switch err.Tag() {
 | 
			
		||||
		case "required":
 | 
			
		||||
			errsStr[shortenNamespace(err.Namespace())] = fmt.Sprintf("Field \"%s\" is required", err.Field())
 | 
			
		||||
		case "gte":
 | 
			
		||||
			errsStr[shortenNamespace(err.Namespace())] = fmt.Sprintf("Value of \"%s\" must be greater than or equal to \"%s\"", err.Field(), err.Param())
 | 
			
		||||
		case "lte":
 | 
			
		||||
			errsStr[shortenNamespace(err.Namespace())] = fmt.Sprintf("Value of \"%s\" must be less than or equal to \"%s\"", err.Field(), err.Param())
 | 
			
		||||
		case "filepath":
 | 
			
		||||
			errsStr[shortenNamespace(err.Namespace())] = fmt.Sprintf("Invalid file path for \"%s\" field: \"%s\"", err.Field(), err.Value())
 | 
			
		||||
		case "http_url":
 | 
			
		||||
			errsStr[shortenNamespace(err.Namespace())] =
 | 
			
		||||
				fmt.Sprintf("Invalid url for \"%s\" field: \"%s\"", err.Field(), err.Value())
 | 
			
		||||
		// --------------------------------------| Custom validations |--------------------------------------
 | 
			
		||||
		case "http_method":
 | 
			
		||||
			errsStr[shortenNamespace(err.Namespace())] = fmt.Sprintf("Invalid HTTP method for \"%s\" field: \"%s\"", err.Field(), err.Value())
 | 
			
		||||
		case "proxy_url":
 | 
			
		||||
			errsStr[shortenNamespace(err.Namespace())] = fmt.Sprintf("Invalid proxy url for \"%s\" field: \"%s\" (it must be http, socks5 or socks5h)", err.Field(), err.Value())
 | 
			
		||||
		case "string_bool":
 | 
			
		||||
			errsStr[shortenNamespace(err.Namespace())] = fmt.Sprintf("Invalid value for \"%s\" field: \"%s\"", err.Field(), err.Value())
 | 
			
		||||
		default:
 | 
			
		||||
			errsStr[shortenNamespace(err.Namespace())] = fmt.Sprintf("Invalid value for \"%s\" field: \"%s\"", err.Field(), err.Value())
 | 
			
		||||
		}
 | 
			
		||||
	}
 | 
			
		||||
	return NewValidationErrors(errsStr, errs)
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
func RequestErrorsFormater(err error) string {
 | 
			
		||||
	switch e := err.(type) {
 | 
			
		||||
	case *url.Error:
 | 
			
		||||
		if netErr, ok := e.Err.(net.Error); ok && netErr.Timeout() {
 | 
			
		||||
			return "Timeout Error"
 | 
			
		||||
		}
 | 
			
		||||
		if strings.Contains(e.Error(), "http: ContentLength=") {
 | 
			
		||||
			println(e.Error())
 | 
			
		||||
			return "Empty Body Error"
 | 
			
		||||
		}
 | 
			
		||||
		// TODO: Add more cases
 | 
			
		||||
	}
 | 
			
		||||
	return "Unknown Error"
 | 
			
		||||
}
 | 
			
		||||
							
								
								
									
										28
									
								
								go.mod
									
									
									
									
									
								
							
							
						
						
									
										28
									
								
								go.mod
									
									
									
									
									
								
							@@ -1,26 +1,22 @@
 | 
			
		||||
module github.com/aykhans/dodo
 | 
			
		||||
 | 
			
		||||
go 1.23.2
 | 
			
		||||
go 1.25
 | 
			
		||||
 | 
			
		||||
require (
 | 
			
		||||
	github.com/go-playground/validator/v10 v10.23.0
 | 
			
		||||
	github.com/jedib0t/go-pretty/v6 v6.6.5
 | 
			
		||||
	github.com/valyala/fasthttp v1.58.0
 | 
			
		||||
	golang.org/x/net v0.33.0
 | 
			
		||||
	github.com/brianvoe/gofakeit/v7 v7.3.0
 | 
			
		||||
	github.com/jedib0t/go-pretty/v6 v6.6.8
 | 
			
		||||
	github.com/valyala/fasthttp v1.68.0
 | 
			
		||||
	gopkg.in/yaml.v3 v3.0.1
 | 
			
		||||
)
 | 
			
		||||
 | 
			
		||||
require (
 | 
			
		||||
	github.com/andybalholm/brotli v1.1.1 // indirect
 | 
			
		||||
	github.com/gabriel-vasile/mimetype v1.4.4 // indirect
 | 
			
		||||
	github.com/go-playground/locales v0.14.1 // indirect
 | 
			
		||||
	github.com/go-playground/universal-translator v0.18.1 // indirect
 | 
			
		||||
	github.com/klauspost/compress v1.17.11 // indirect
 | 
			
		||||
	github.com/leodido/go-urn v1.4.0 // indirect
 | 
			
		||||
	github.com/mattn/go-runewidth v0.0.15 // indirect
 | 
			
		||||
	github.com/andybalholm/brotli v1.2.0 // indirect
 | 
			
		||||
	github.com/klauspost/compress v1.18.1 // indirect
 | 
			
		||||
	github.com/mattn/go-runewidth v0.0.16 // indirect
 | 
			
		||||
	github.com/rivo/uniseg v0.4.7 // indirect
 | 
			
		||||
	github.com/valyala/bytebufferpool v1.0.0 // indirect
 | 
			
		||||
	golang.org/x/crypto v0.31.0 // indirect
 | 
			
		||||
	golang.org/x/sys v0.28.0 // indirect
 | 
			
		||||
	golang.org/x/term v0.27.0 // indirect
 | 
			
		||||
	golang.org/x/text v0.21.0 // indirect
 | 
			
		||||
	golang.org/x/net v0.46.0 // indirect
 | 
			
		||||
	golang.org/x/sys v0.37.0 // indirect
 | 
			
		||||
	golang.org/x/term v0.36.0 // indirect
 | 
			
		||||
	golang.org/x/text v0.30.0 // indirect
 | 
			
		||||
)
 | 
			
		||||
 
 | 
			
		||||
							
								
								
									
										58
									
								
								go.sum
									
									
									
									
									
								
							
							
						
						
									
										58
									
								
								go.sum
									
									
									
									
									
								
							@@ -1,47 +1,37 @@
 | 
			
		||||
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.2.0 h1:ukwgCxwYrmACq68yiUqwIWnGY0cTPox/M94sVwToPjQ=
 | 
			
		||||
github.com/andybalholm/brotli v1.2.0/go.mod h1:rzTDkvFWvIrjDXZHkuS16NPggd91W3kUSvPlQ1pLaKY=
 | 
			
		||||
github.com/brianvoe/gofakeit/v7 v7.3.0 h1:TWStf7/lLpAjKw+bqwzeORo9jvrxToWEwp9b1J2vApQ=
 | 
			
		||||
github.com/brianvoe/gofakeit/v7 v7.3.0/go.mod h1:QXuPeBw164PJCzCUZVmgpgHJ3Llj49jSLVkKPMtxtxA=
 | 
			
		||||
github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c=
 | 
			
		||||
github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
 | 
			
		||||
github.com/gabriel-vasile/mimetype v1.4.4 h1:QjV6pZ7/XZ7ryI2KuyeEDE8wnh7fHP9YnQy+R0LnH8I=
 | 
			
		||||
github.com/gabriel-vasile/mimetype v1.4.4/go.mod h1:JwLei5XPtWdGiMFB5Pjle1oEeoSeEuJfJE+TtfvdB/s=
 | 
			
		||||
github.com/go-playground/assert/v2 v2.2.0 h1:JvknZsQTYeFEAhQwI4qEt9cyV5ONwRHC+lYKSsYSR8s=
 | 
			
		||||
github.com/go-playground/assert/v2 v2.2.0/go.mod h1:VDjEfimB/XKnb+ZQfWdccd7VUvScMdVu0Titje2rxJ4=
 | 
			
		||||
github.com/go-playground/locales v0.14.1 h1:EWaQ/wswjilfKLTECiXz7Rh+3BjFhfDFKv/oXslEjJA=
 | 
			
		||||
github.com/go-playground/locales v0.14.1/go.mod h1:hxrqLVvrK65+Rwrd5Fc6F2O76J/NuW9t0sjnWqG1slY=
 | 
			
		||||
github.com/go-playground/universal-translator v0.18.1 h1:Bcnm0ZwsGyWbCzImXv+pAJnYK9S473LQFuzCbDbfSFY=
 | 
			
		||||
github.com/go-playground/universal-translator v0.18.1/go.mod h1:xekY+UJKNuX9WP91TpwSH2VMlDf28Uj24BCp08ZFTUY=
 | 
			
		||||
github.com/go-playground/validator/v10 v10.23.0 h1:/PwmTwZhS0dPkav3cdK9kV1FsAmrL8sThn8IHr/sO+o=
 | 
			
		||||
github.com/go-playground/validator/v10 v10.23.0/go.mod h1:dbuPbCMFw/DrkbEynArYaCwl3amGuJotoKCe95atGMM=
 | 
			
		||||
github.com/jedib0t/go-pretty/v6 v6.6.5 h1:9PgMJOVBedpgYLI56jQRJYqngxYAAzfEUua+3NgSqAo=
 | 
			
		||||
github.com/jedib0t/go-pretty/v6 v6.6.5/go.mod h1:Uq/HrbhuFty5WSVNfjpQQe47x16RwVGXIveNGEyGtHs=
 | 
			
		||||
github.com/klauspost/compress v1.17.11 h1:In6xLpyWOi1+C7tXUUWv2ot1QvBjxevKAaI6IXrJmUc=
 | 
			
		||||
github.com/klauspost/compress v1.17.11/go.mod h1:pMDklpSncoRMuLFrf1W9Ss9KT+0rH90U12bZKk7uwG0=
 | 
			
		||||
github.com/leodido/go-urn v1.4.0 h1:WT9HwE9SGECu3lg4d/dIA+jxlljEa1/ffXKmRjqdmIQ=
 | 
			
		||||
github.com/leodido/go-urn v1.4.0/go.mod h1:bvxc+MVxLKB4z00jd1z+Dvzr47oO32F/QSNjSBOlFxI=
 | 
			
		||||
github.com/mattn/go-runewidth v0.0.15 h1:UNAjwbU9l54TA3KzvqLGxwWjHmMgBUVhBiTjelZgg3U=
 | 
			
		||||
github.com/mattn/go-runewidth v0.0.15/go.mod h1:Jdepj2loyihRzMpdS35Xk/zdY8IAYHsh153qUoGf23w=
 | 
			
		||||
github.com/jedib0t/go-pretty/v6 v6.6.8 h1:JnnzQeRz2bACBobIaa/r+nqjvws4yEhcmaZ4n1QzsEc=
 | 
			
		||||
github.com/jedib0t/go-pretty/v6 v6.6.8/go.mod h1:YwC5CE4fJ1HFUDeivSV1r//AmANFHyqczZk+U6BDALU=
 | 
			
		||||
github.com/klauspost/compress v1.18.1 h1:bcSGx7UbpBqMChDtsF28Lw6v/G94LPrrbMbdC3JH2co=
 | 
			
		||||
github.com/klauspost/compress v1.18.1/go.mod h1:ZQFFVG+MdnR0P+l6wpXgIL4NTtwiKIdBnrBd8Nrxr+0=
 | 
			
		||||
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/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM=
 | 
			
		||||
github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4=
 | 
			
		||||
github.com/rivo/uniseg v0.2.0/go.mod h1:J6wj4VEh+S6ZtnVlnTBMWIodfgj8LQOQFoIToxlJtxc=
 | 
			
		||||
github.com/rivo/uniseg v0.4.7 h1:WUdvkW8uEhrYfLC4ZzdpI2ztxP1I582+49Oc5Mq64VQ=
 | 
			
		||||
github.com/rivo/uniseg v0.4.7/go.mod h1:FN3SvrM+Zdj16jyLfmOkMNblXMcoc8DfTHruCPUcx88=
 | 
			
		||||
github.com/stretchr/testify v1.8.4 h1:CcVxjf3Q8PM0mHUKJCdn+eZZtm5yQwehR5yeSVQQcUk=
 | 
			
		||||
github.com/stretchr/testify v1.8.4/go.mod h1:sz/lmYIOXD/1dqDmKjjqLyZ2RngseejIcXlSw2iwfAo=
 | 
			
		||||
github.com/stretchr/testify v1.10.0 h1:Xv5erBjTwe/5IxqUQTdXv5kgmIvbHo3QQyRwhJsOfJA=
 | 
			
		||||
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/go.mod h1:6bBcMArwyJ5K/AmCkWv1jt77kVWyCJ6HpOuEn7z0Csc=
 | 
			
		||||
github.com/valyala/fasthttp v1.58.0 h1:GGB2dWxSbEprU9j0iMJHgdKYJVDyjrOwF9RE59PbRuE=
 | 
			
		||||
github.com/valyala/fasthttp v1.58.0/go.mod h1:SYXvHHaFp7QZHGKSHmoMipInhrI5StHrhDTYVEjK/Kw=
 | 
			
		||||
github.com/valyala/fasthttp v1.68.0 h1:v12Nx16iepr8r9ySOwqI+5RBJ/DqTxhOy1HrHoDFnok=
 | 
			
		||||
github.com/valyala/fasthttp v1.68.0/go.mod h1:5EXiRfYQAoiO/khu4oU9VISC/eVY6JqmSpPJoHCKsz4=
 | 
			
		||||
github.com/xyproto/randomstring v1.0.5 h1:YtlWPoRdgMu3NZtP45drfy1GKoojuR7hmRcnhZqKjWU=
 | 
			
		||||
github.com/xyproto/randomstring v1.0.5/go.mod h1:rgmS5DeNXLivK7YprL0pY+lTuhNQW3iGxZ18UQApw/E=
 | 
			
		||||
golang.org/x/crypto v0.31.0 h1:ihbySMvVjLAeSH1IbfcRTkD/iNscyz8rGzjF/E5hV6U=
 | 
			
		||||
golang.org/x/crypto v0.31.0/go.mod h1:kDsLvtWBEx7MV9tJOj9bnXsPbxwJQ6csT/x4KIN4Ssk=
 | 
			
		||||
golang.org/x/net v0.33.0 h1:74SYHlV8BIgHIFC/LrYkOGIwL19eTYXQ5wc6TBuO36I=
 | 
			
		||||
golang.org/x/net v0.33.0/go.mod h1:HXLR5J+9DxmrqMwG9qjGCxZ+zKXxBru04zlTvWlWuN4=
 | 
			
		||||
golang.org/x/sys v0.28.0 h1:Fksou7UEQUWlKvIdsqzJmUmCX3cZuD2+P3XyyzwMhlA=
 | 
			
		||||
golang.org/x/sys v0.28.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA=
 | 
			
		||||
golang.org/x/term v0.27.0 h1:WP60Sv1nlK1T6SupCHbXzSaN0b9wUmsPoRS9b61A23Q=
 | 
			
		||||
golang.org/x/term v0.27.0/go.mod h1:iMsnZpn0cago0GOrHO2+Y7u7JPn5AylBrcoWkElMTSM=
 | 
			
		||||
golang.org/x/text v0.21.0 h1:zyQAAkrwaneQ066sspRyJaG9VNi/YJ1NfzcGB3hZ/qo=
 | 
			
		||||
golang.org/x/text v0.21.0/go.mod h1:4IBbMaMmOPCJ8SecivzSH54+73PCFmPWxNTLm+vZkEQ=
 | 
			
		||||
golang.org/x/net v0.46.0 h1:giFlY12I07fugqwPuWJi68oOnpfqFnJIJzaIIm2JVV4=
 | 
			
		||||
golang.org/x/net v0.46.0/go.mod h1:Q9BGdFy1y4nkUwiLvT5qtyhAnEHgnQ/zd8PfU6nc210=
 | 
			
		||||
golang.org/x/sys v0.37.0 h1:fdNQudmxPjkdUTPnLn5mdQv7Zwvbvpaxqs831goi9kQ=
 | 
			
		||||
golang.org/x/sys v0.37.0/go.mod h1:OgkHotnGiDImocRcuBABYBEXf8A9a87e/uXjp9XT3ks=
 | 
			
		||||
golang.org/x/term v0.36.0 h1:zMPR+aF8gfksFprF/Nc/rd1wRS1EI6nDBGyWAvDzx2Q=
 | 
			
		||||
golang.org/x/term v0.36.0/go.mod h1:Qu394IJq6V6dCBRgwqshf3mPF85AqzYEzofzRdZkWss=
 | 
			
		||||
golang.org/x/text v0.30.0 h1:yznKA/E9zq54KzlzBEAWn1NXSQ8DIp/NYMy88xJjl4k=
 | 
			
		||||
golang.org/x/text v0.30.0/go.mod h1:yDdHFIX9t+tORqspjENWgzaCVXgk0yYnYuSZ8UzzBVM=
 | 
			
		||||
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/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA=
 | 
			
		||||
gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
 | 
			
		||||
 
 | 
			
		||||
							
								
								
									
										117
									
								
								main.go
									
									
									
									
									
								
							
							
						
						
									
										117
									
								
								main.go
									
									
									
									
									
								
							@@ -2,121 +2,68 @@ package main
 | 
			
		||||
 | 
			
		||||
import (
 | 
			
		||||
	"context"
 | 
			
		||||
	"errors"
 | 
			
		||||
	"fmt"
 | 
			
		||||
	"net/url"
 | 
			
		||||
	"os"
 | 
			
		||||
	"os/signal"
 | 
			
		||||
	"strings"
 | 
			
		||||
	"syscall"
 | 
			
		||||
	"time"
 | 
			
		||||
 | 
			
		||||
	"github.com/aykhans/dodo/config"
 | 
			
		||||
	customerrors "github.com/aykhans/dodo/custom_errors"
 | 
			
		||||
	"github.com/aykhans/dodo/readers"
 | 
			
		||||
	"github.com/aykhans/dodo/requests"
 | 
			
		||||
	"github.com/aykhans/dodo/types"
 | 
			
		||||
	"github.com/aykhans/dodo/utils"
 | 
			
		||||
	"github.com/aykhans/dodo/validation"
 | 
			
		||||
	goValidator "github.com/go-playground/validator/v10"
 | 
			
		||||
	"github.com/jedib0t/go-pretty/v6/text"
 | 
			
		||||
)
 | 
			
		||||
 | 
			
		||||
func main() {
 | 
			
		||||
	validator := validation.NewValidator()
 | 
			
		||||
	conf := config.NewConfig("", 0, 0, 0, nil)
 | 
			
		||||
	jsonConf := config.NewJSONConfig(
 | 
			
		||||
		config.NewConfig("", 0, 0, 0, nil), nil, nil, nil, nil, nil,
 | 
			
		||||
	)
 | 
			
		||||
 | 
			
		||||
	cliConf, err := readers.CLIConfigReader()
 | 
			
		||||
	if err != nil {
 | 
			
		||||
		utils.PrintAndExit(err.Error())
 | 
			
		||||
	}
 | 
			
		||||
	if cliConf == nil {
 | 
			
		||||
		os.Exit(0)
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	if err := validator.StructPartial(cliConf, "ConfigFile"); err != nil {
 | 
			
		||||
		utils.PrintErrAndExit(
 | 
			
		||||
			customerrors.ValidationErrorsFormater(
 | 
			
		||||
				err.(goValidator.ValidationErrors),
 | 
			
		||||
			),
 | 
			
		||||
		)
 | 
			
		||||
	}
 | 
			
		||||
	if cliConf.ConfigFile != "" {
 | 
			
		||||
		jsonConfNew, err := readers.JSONConfigReader(cliConf.ConfigFile)
 | 
			
		||||
	conf := config.NewConfig()
 | 
			
		||||
	configFile, err := conf.ReadCLI()
 | 
			
		||||
	if err != nil {
 | 
			
		||||
		utils.PrintErrAndExit(err)
 | 
			
		||||
	}
 | 
			
		||||
		if err := validator.StructFiltered(
 | 
			
		||||
			jsonConfNew,
 | 
			
		||||
			func(ns []byte) bool {
 | 
			
		||||
				return strings.LastIndex(string(ns), "Proxies") == -1
 | 
			
		||||
			}); err != nil {
 | 
			
		||||
			utils.PrintErrAndExit(
 | 
			
		||||
				customerrors.ValidationErrorsFormater(
 | 
			
		||||
					err.(goValidator.ValidationErrors),
 | 
			
		||||
				),
 | 
			
		||||
			)
 | 
			
		||||
		}
 | 
			
		||||
		jsonConf = jsonConfNew
 | 
			
		||||
		conf.MergeConfigs(jsonConf.Config)
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	conf.MergeConfigs(cliConf.Config)
 | 
			
		||||
	if configFile.String() != "" {
 | 
			
		||||
		tempConf := config.NewConfig()
 | 
			
		||||
		if err := tempConf.ReadFile(configFile); err != nil {
 | 
			
		||||
			utils.PrintErrAndExit(err)
 | 
			
		||||
		}
 | 
			
		||||
		tempConf.MergeConfig(conf)
 | 
			
		||||
		conf = tempConf
 | 
			
		||||
	}
 | 
			
		||||
	conf.SetDefaults()
 | 
			
		||||
	if err := validator.Struct(conf); err != nil {
 | 
			
		||||
		utils.PrintErrAndExit(
 | 
			
		||||
			customerrors.ValidationErrorsFormater(
 | 
			
		||||
				err.(goValidator.ValidationErrors),
 | 
			
		||||
			),
 | 
			
		||||
		)
 | 
			
		||||
 | 
			
		||||
	if errs := conf.Validate(); len(errs) > 0 {
 | 
			
		||||
		utils.PrintErrAndExit(errors.Join(errs...))
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	parsedURL, err := url.Parse(conf.URL)
 | 
			
		||||
	if err != nil {
 | 
			
		||||
		utils.PrintErrAndExit(err)
 | 
			
		||||
	}
 | 
			
		||||
	requestConf := &config.RequestConfig{
 | 
			
		||||
		Method:       conf.Method,
 | 
			
		||||
		URL:          parsedURL,
 | 
			
		||||
		Timeout:      time.Duration(conf.Timeout) * time.Millisecond,
 | 
			
		||||
		DodosCount:   conf.DodosCount,
 | 
			
		||||
		RequestCount: conf.RequestCount,
 | 
			
		||||
		Params:       jsonConf.Params,
 | 
			
		||||
		Headers:      jsonConf.Headers,
 | 
			
		||||
		Cookies:      jsonConf.Cookies,
 | 
			
		||||
		Proxies:      jsonConf.Proxies,
 | 
			
		||||
		Body:         jsonConf.Body,
 | 
			
		||||
		Yes:          cliConf.Yes.ValueOr(false),
 | 
			
		||||
		NoProxyCheck: conf.NoProxyCheck.ValueOr(false),
 | 
			
		||||
	}
 | 
			
		||||
	requestConf := config.NewRequestConfig(conf)
 | 
			
		||||
	requestConf.Print()
 | 
			
		||||
	if !cliConf.Yes.ValueOr(false) {
 | 
			
		||||
		response := readers.CLIYesOrNoReader("Do you want to continue?", true)
 | 
			
		||||
 | 
			
		||||
	if !requestConf.Yes {
 | 
			
		||||
		response := config.CLIYesOrNoReader("Do you want to continue?", false)
 | 
			
		||||
		if !response {
 | 
			
		||||
			utils.PrintAndExit("Exiting...")
 | 
			
		||||
			utils.PrintAndExit("Exiting...\n")
 | 
			
		||||
		}
 | 
			
		||||
		fmt.Println()
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	ctx, cancel := context.WithCancel(context.Background())
 | 
			
		||||
	sigChan := make(chan os.Signal, 1)
 | 
			
		||||
	signal.Notify(sigChan, syscall.SIGINT, syscall.SIGTERM)
 | 
			
		||||
	go func() {
 | 
			
		||||
		<-sigChan
 | 
			
		||||
		cancel()
 | 
			
		||||
	}()
 | 
			
		||||
	go listenForTermination(func() { cancel() })
 | 
			
		||||
 | 
			
		||||
	responses, err := requests.Run(ctx, requestConf)
 | 
			
		||||
	if err != nil {
 | 
			
		||||
		if customerrors.Is(err, customerrors.ErrInterrupt) {
 | 
			
		||||
			utils.PrintlnC(utils.Colors.Yellow, err.Error())
 | 
			
		||||
			return
 | 
			
		||||
		} else if customerrors.Is(err, customerrors.ErrNoInternet) {
 | 
			
		||||
			utils.PrintAndExit("No internet connection")
 | 
			
		||||
		if err == types.ErrInterrupt {
 | 
			
		||||
			fmt.Println(text.FgYellow.Sprint(err.Error()))
 | 
			
		||||
			return
 | 
			
		||||
		}
 | 
			
		||||
		panic(err)
 | 
			
		||||
		utils.PrintErrAndExit(err)
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	responses.Print()
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
func listenForTermination(do func()) {
 | 
			
		||||
	sigChan := make(chan os.Signal, 1)
 | 
			
		||||
	signal.Notify(sigChan, syscall.SIGINT, syscall.SIGTERM)
 | 
			
		||||
	<-sigChan
 | 
			
		||||
	do()
 | 
			
		||||
}
 | 
			
		||||
 
 | 
			
		||||
							
								
								
									
										155
									
								
								readers/cli.go
									
									
									
									
									
								
							
							
						
						
									
										155
									
								
								readers/cli.go
									
									
									
									
									
								
							@@ -1,155 +0,0 @@
 | 
			
		||||
package readers
 | 
			
		||||
 | 
			
		||||
import (
 | 
			
		||||
	"flag"
 | 
			
		||||
	"fmt"
 | 
			
		||||
	"strings"
 | 
			
		||||
 | 
			
		||||
	"github.com/aykhans/dodo/config"
 | 
			
		||||
	. "github.com/aykhans/dodo/types"
 | 
			
		||||
	"github.com/aykhans/dodo/utils"
 | 
			
		||||
)
 | 
			
		||||
 | 
			
		||||
const usageText = `Usage:
 | 
			
		||||
  dodo [flags]
 | 
			
		||||
 | 
			
		||||
Examples:
 | 
			
		||||
 | 
			
		||||
Simple usage only with URL:
 | 
			
		||||
  dodo -u https://example.com
 | 
			
		||||
 | 
			
		||||
Simple usage with config file:
 | 
			
		||||
  dodo -c /path/to/config/file/config.json
 | 
			
		||||
 | 
			
		||||
Usage with all flags:
 | 
			
		||||
  dodo -c /path/to/config/file/config.json -u https://example.com -m POST -d 10 -r 1000 -t 2000 --no-proxy-check -y
 | 
			
		||||
 | 
			
		||||
Flags:
 | 
			
		||||
  -h, --help                 help for dodo
 | 
			
		||||
  -v, --version              version for dodo
 | 
			
		||||
  -c, --config-file string   Path to the config file
 | 
			
		||||
  -d, --dodos-count uint     Number of dodos(threads) (default %d)
 | 
			
		||||
  -m, --method string        HTTP Method (default %s)
 | 
			
		||||
  -r, --request-count uint   Number of total requests (default %d)
 | 
			
		||||
  -t, --timeout uint32       Timeout for each request in milliseconds (default %d)
 | 
			
		||||
  -u, --url string           URL for stress testing
 | 
			
		||||
      --no-proxy-check bool  Do not check for proxies (default false)
 | 
			
		||||
  -y, --yes bool             Answer yes to all questions (default false)`
 | 
			
		||||
 | 
			
		||||
func CLIConfigReader() (*config.CLIConfig, error) {
 | 
			
		||||
	flag.Usage = func() {
 | 
			
		||||
		fmt.Printf(
 | 
			
		||||
			usageText+"\n",
 | 
			
		||||
			config.DefaultDodosCount,
 | 
			
		||||
			config.DefaultMethod,
 | 
			
		||||
			config.DefaultRequestCount,
 | 
			
		||||
			config.DefaultTimeout,
 | 
			
		||||
		)
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	var (
 | 
			
		||||
		cliConfig          = config.NewCLIConfig(config.NewConfig("", 0, 0, 0, nil), NewOption(false), "")
 | 
			
		||||
		configFile         = ""
 | 
			
		||||
		yes                = false
 | 
			
		||||
		method             = ""
 | 
			
		||||
		url                = ""
 | 
			
		||||
		dodosCount    uint = 0
 | 
			
		||||
		requestsCount uint = 0
 | 
			
		||||
		timeout       uint = 0
 | 
			
		||||
		noProxyCheck  bool = false
 | 
			
		||||
	)
 | 
			
		||||
	{
 | 
			
		||||
		flag.Bool("version", false, "Prints the version of the program")
 | 
			
		||||
		flag.Bool("v", false, "Prints the version of the program")
 | 
			
		||||
 | 
			
		||||
		flag.StringVar(&configFile, "config-file", "", "Path to the configuration file")
 | 
			
		||||
		flag.StringVar(&configFile, "c", "", "Path to the configuration file")
 | 
			
		||||
 | 
			
		||||
		flag.BoolVar(&yes, "yes", false, "Answer yes to all questions")
 | 
			
		||||
		flag.BoolVar(&yes, "y", false, "Answer yes to all questions")
 | 
			
		||||
 | 
			
		||||
		flag.StringVar(&method, "method", "", "HTTP Method")
 | 
			
		||||
		flag.StringVar(&method, "m", "", "HTTP Method")
 | 
			
		||||
 | 
			
		||||
		flag.StringVar(&url, "url", "", "URL to send the request")
 | 
			
		||||
		flag.StringVar(&url, "u", "", "URL to send the request")
 | 
			
		||||
 | 
			
		||||
		flag.UintVar(&dodosCount, "dodos-count", 0, "Number of dodos(threads)")
 | 
			
		||||
		flag.UintVar(&dodosCount, "d", 0, "Number of dodos(threads)")
 | 
			
		||||
 | 
			
		||||
		flag.UintVar(&requestsCount, "requests-count", 0, "Number of total requests")
 | 
			
		||||
		flag.UintVar(&requestsCount, "r", 0, "Number of total requests")
 | 
			
		||||
 | 
			
		||||
		flag.UintVar(&timeout, "timeout", 0, "Timeout for each request in milliseconds")
 | 
			
		||||
		flag.UintVar(&timeout, "t", 0, "Timeout for each request in milliseconds")
 | 
			
		||||
 | 
			
		||||
		flag.BoolVar(&noProxyCheck, "no-proxy-check", false, "Do not check for active proxies")
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	flag.Parse()
 | 
			
		||||
 | 
			
		||||
	args := flag.Args()
 | 
			
		||||
	if len(args) > 0 {
 | 
			
		||||
		return nil, fmt.Errorf("unexpected arguments: %v", strings.Join(args, ", "))
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	returnNil := false
 | 
			
		||||
	flag.Visit(func(f *flag.Flag) {
 | 
			
		||||
		switch f.Name {
 | 
			
		||||
		case "version", "v":
 | 
			
		||||
			fmt.Printf("dodo version %s\n", config.VERSION)
 | 
			
		||||
			returnNil = true
 | 
			
		||||
		case "config-file", "c":
 | 
			
		||||
			cliConfig.ConfigFile = configFile
 | 
			
		||||
		case "yes", "y":
 | 
			
		||||
			cliConfig.Yes.SetValue(yes)
 | 
			
		||||
		case "method", "m":
 | 
			
		||||
			cliConfig.Method = method
 | 
			
		||||
		case "url", "u":
 | 
			
		||||
			cliConfig.URL = url
 | 
			
		||||
		case "dodos-count", "d":
 | 
			
		||||
			cliConfig.DodosCount = dodosCount
 | 
			
		||||
		case "requests-count", "r":
 | 
			
		||||
			cliConfig.RequestCount = requestsCount
 | 
			
		||||
		case "timeout", "t":
 | 
			
		||||
			var maxUint32 uint = 4294967295
 | 
			
		||||
			if timeout > maxUint32 {
 | 
			
		||||
				utils.PrintfC(utils.Colors.Yellow, "timeout value is too large, setting to %d\n", maxUint32)
 | 
			
		||||
				timeout = maxUint32
 | 
			
		||||
			}
 | 
			
		||||
			cliConfig.Timeout = uint32(timeout)
 | 
			
		||||
		case "no-proxy-check":
 | 
			
		||||
			cliConfig.NoProxyCheck.SetValue(noProxyCheck)
 | 
			
		||||
		}
 | 
			
		||||
	})
 | 
			
		||||
 | 
			
		||||
	if returnNil {
 | 
			
		||||
		return nil, nil
 | 
			
		||||
	}
 | 
			
		||||
	return cliConfig, nil
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
// CLIYesOrNoReader reads a yes or no answer from the command line.
 | 
			
		||||
// It prompts the user with the given message and default value,
 | 
			
		||||
// and returns true if the user answers "y" or "Y", and false otherwise.
 | 
			
		||||
// If there is an error while reading the input, it returns false.
 | 
			
		||||
// If the user simply presses enter without providing any input,
 | 
			
		||||
// it returns the default value specified by the `dft` parameter.
 | 
			
		||||
func CLIYesOrNoReader(message string, dft bool) bool {
 | 
			
		||||
	var answer string
 | 
			
		||||
	defaultMessage := "Y/n"
 | 
			
		||||
	if !dft {
 | 
			
		||||
		defaultMessage = "y/N"
 | 
			
		||||
	}
 | 
			
		||||
	fmt.Printf("%s [%s]: ", message, defaultMessage)
 | 
			
		||||
	if _, err := fmt.Scanln(&answer); err != nil {
 | 
			
		||||
		if err.Error() == "unexpected newline" {
 | 
			
		||||
			return dft
 | 
			
		||||
		}
 | 
			
		||||
		return false
 | 
			
		||||
	}
 | 
			
		||||
	if answer == "" {
 | 
			
		||||
		return dft
 | 
			
		||||
	}
 | 
			
		||||
	return answer == "y" || answer == "Y"
 | 
			
		||||
}
 | 
			
		||||
@@ -1,36 +0,0 @@
 | 
			
		||||
package readers
 | 
			
		||||
 | 
			
		||||
import (
 | 
			
		||||
	"encoding/json"
 | 
			
		||||
	"os"
 | 
			
		||||
 | 
			
		||||
	"github.com/aykhans/dodo/config"
 | 
			
		||||
	customerrors "github.com/aykhans/dodo/custom_errors"
 | 
			
		||||
)
 | 
			
		||||
 | 
			
		||||
func JSONConfigReader(filePath string) (*config.JSONConfig, error) {
 | 
			
		||||
	data, err := os.ReadFile(filePath)
 | 
			
		||||
	if err != nil {
 | 
			
		||||
		return nil, customerrors.OSErrorFormater(err)
 | 
			
		||||
	}
 | 
			
		||||
	jsonConf := config.NewJSONConfig(
 | 
			
		||||
		config.NewConfig("", 0, 0, 0, nil),
 | 
			
		||||
		nil, nil, nil, nil, nil,
 | 
			
		||||
	)
 | 
			
		||||
	err = json.Unmarshal(data, &jsonConf)
 | 
			
		||||
 | 
			
		||||
	if err != nil {
 | 
			
		||||
		switch err := err.(type) {
 | 
			
		||||
		case *json.UnmarshalTypeError:
 | 
			
		||||
			return nil,
 | 
			
		||||
				customerrors.NewTypeError(
 | 
			
		||||
					err.Type.String(),
 | 
			
		||||
					err.Value,
 | 
			
		||||
					err.Field,
 | 
			
		||||
					err,
 | 
			
		||||
				)
 | 
			
		||||
		}
 | 
			
		||||
		return nil, customerrors.NewInvalidFileError(filePath, err)
 | 
			
		||||
	}
 | 
			
		||||
	return jsonConf, nil
 | 
			
		||||
}
 | 
			
		||||
@@ -2,14 +2,12 @@ package requests
 | 
			
		||||
 | 
			
		||||
import (
 | 
			
		||||
	"context"
 | 
			
		||||
	"fmt"
 | 
			
		||||
	"crypto/tls"
 | 
			
		||||
	"errors"
 | 
			
		||||
	"math/rand"
 | 
			
		||||
	"net/url"
 | 
			
		||||
	"sync"
 | 
			
		||||
	"time"
 | 
			
		||||
 | 
			
		||||
	"github.com/aykhans/dodo/config"
 | 
			
		||||
	"github.com/aykhans/dodo/readers"
 | 
			
		||||
	"github.com/aykhans/dodo/utils"
 | 
			
		||||
	"github.com/valyala/fasthttp"
 | 
			
		||||
	"github.com/valyala/fasthttp/fasthttpproxy"
 | 
			
		||||
@@ -20,20 +18,16 @@ type ClientGeneratorFunc func() *fasthttp.HostClient
 | 
			
		||||
// 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.
 | 
			
		||||
func getClients(
 | 
			
		||||
	ctx context.Context,
 | 
			
		||||
	_ context.Context,
 | 
			
		||||
	timeout time.Duration,
 | 
			
		||||
	proxies []config.Proxy,
 | 
			
		||||
	dodosCount uint,
 | 
			
		||||
	proxies []url.URL,
 | 
			
		||||
	maxConns uint,
 | 
			
		||||
	yes bool,
 | 
			
		||||
	noProxyCheck bool,
 | 
			
		||||
	URL *url.URL,
 | 
			
		||||
	URL url.URL,
 | 
			
		||||
	skipVerify bool,
 | 
			
		||||
) []*fasthttp.HostClient {
 | 
			
		||||
	isTLS := URL.Scheme == "https"
 | 
			
		||||
 | 
			
		||||
	if proxiesLen := len(proxies); proxiesLen > 0 {
 | 
			
		||||
		// If noProxyCheck is true, we will return the clients without checking the proxies.
 | 
			
		||||
		if noProxyCheck {
 | 
			
		||||
		clients := make([]*fasthttp.HostClient, 0, proxiesLen)
 | 
			
		||||
		addr := URL.Host
 | 
			
		||||
		if isTLS && URL.Port() == "" {
 | 
			
		||||
@@ -49,6 +43,9 @@ func getClients(
 | 
			
		||||
			clients = append(clients, &fasthttp.HostClient{
 | 
			
		||||
				MaxConns: int(maxConns),
 | 
			
		||||
				IsTLS:    isTLS,
 | 
			
		||||
				TLSConfig: &tls.Config{
 | 
			
		||||
					InsecureSkipVerify: skipVerify,
 | 
			
		||||
				},
 | 
			
		||||
				Addr:                addr,
 | 
			
		||||
				Dial:                dialFunc,
 | 
			
		||||
				MaxIdleConnDuration: timeout,
 | 
			
		||||
@@ -61,46 +58,12 @@ func getClients(
 | 
			
		||||
		return clients
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
		// Else, we will check the proxies and return the active ones.
 | 
			
		||||
		activeProxyClients := getActiveProxyClients(
 | 
			
		||||
			ctx, proxies, timeout, dodosCount, maxConns, URL,
 | 
			
		||||
		)
 | 
			
		||||
		if ctx.Err() != nil {
 | 
			
		||||
			return nil
 | 
			
		||||
		}
 | 
			
		||||
		activeProxyClientsCount := uint(len(activeProxyClients))
 | 
			
		||||
		var yesOrNoMessage string
 | 
			
		||||
		var yesOrNoDefault bool
 | 
			
		||||
		if activeProxyClientsCount == 0 {
 | 
			
		||||
			yesOrNoDefault = false
 | 
			
		||||
			yesOrNoMessage = utils.Colored(
 | 
			
		||||
				utils.Colors.Yellow,
 | 
			
		||||
				"No active proxies found. Do you want to continue?",
 | 
			
		||||
			)
 | 
			
		||||
		} else {
 | 
			
		||||
			yesOrNoMessage = utils.Colored(
 | 
			
		||||
				utils.Colors.Yellow,
 | 
			
		||||
				fmt.Sprintf(
 | 
			
		||||
					"Found %d active proxies. Do you want to continue?",
 | 
			
		||||
					activeProxyClientsCount,
 | 
			
		||||
				),
 | 
			
		||||
			)
 | 
			
		||||
		}
 | 
			
		||||
		if !yes {
 | 
			
		||||
			response := readers.CLIYesOrNoReader("\n"+yesOrNoMessage, yesOrNoDefault)
 | 
			
		||||
			if !response {
 | 
			
		||||
				utils.PrintAndExit("Exiting...")
 | 
			
		||||
			}
 | 
			
		||||
		}
 | 
			
		||||
		fmt.Println()
 | 
			
		||||
		if activeProxyClientsCount > 0 {
 | 
			
		||||
			return activeProxyClients
 | 
			
		||||
		}
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	client := &fasthttp.HostClient{
 | 
			
		||||
		MaxConns: int(maxConns),
 | 
			
		||||
		IsTLS:    isTLS,
 | 
			
		||||
		TLSConfig: &tls.Config{
 | 
			
		||||
			InsecureSkipVerify: skipVerify,
 | 
			
		||||
		},
 | 
			
		||||
		Addr:                URL.Host,
 | 
			
		||||
		MaxIdleConnDuration: timeout,
 | 
			
		||||
		MaxConnDuration:     timeout,
 | 
			
		||||
@@ -110,201 +73,26 @@ func getClients(
 | 
			
		||||
	return []*fasthttp.HostClient{client}
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
// getActiveProxyClients divides the proxies into slices based on the number of dodos and
 | 
			
		||||
// launches goroutines to find active proxy clients for each slice.
 | 
			
		||||
// It uses a progress tracker to monitor the progress of the search.
 | 
			
		||||
// Once all goroutines have completed, the function waits for them to finish and
 | 
			
		||||
// returns a flattened slice of active proxy clients.
 | 
			
		||||
func getActiveProxyClients(
 | 
			
		||||
	ctx context.Context,
 | 
			
		||||
	proxies []config.Proxy,
 | 
			
		||||
	timeout time.Duration,
 | 
			
		||||
	dodosCount uint,
 | 
			
		||||
	maxConns uint,
 | 
			
		||||
	URL *url.URL,
 | 
			
		||||
) []*fasthttp.HostClient {
 | 
			
		||||
	activeProxyClientsArray := make([][]*fasthttp.HostClient, dodosCount)
 | 
			
		||||
	proxiesCount := len(proxies)
 | 
			
		||||
	dodosCountInt := int(dodosCount)
 | 
			
		||||
 | 
			
		||||
	var (
 | 
			
		||||
		wg       sync.WaitGroup
 | 
			
		||||
		streamWG sync.WaitGroup
 | 
			
		||||
	)
 | 
			
		||||
	wg.Add(dodosCountInt)
 | 
			
		||||
	streamWG.Add(1)
 | 
			
		||||
	var proxiesSlice []config.Proxy
 | 
			
		||||
	increase := make(chan int64, proxiesCount)
 | 
			
		||||
 | 
			
		||||
	streamCtx, streamCtxCancel := context.WithCancel(context.Background())
 | 
			
		||||
	go streamProgress(streamCtx, &streamWG, int64(proxiesCount), "Searching for active proxies🌐", increase)
 | 
			
		||||
 | 
			
		||||
	for i := range dodosCountInt {
 | 
			
		||||
		if i+1 == dodosCountInt {
 | 
			
		||||
			proxiesSlice = proxies[i*proxiesCount/dodosCountInt:]
 | 
			
		||||
		} else {
 | 
			
		||||
			proxiesSlice = proxies[i*proxiesCount/dodosCountInt : (i+1)*proxiesCount/dodosCountInt]
 | 
			
		||||
		}
 | 
			
		||||
		go findActiveProxyClients(
 | 
			
		||||
			ctx,
 | 
			
		||||
			proxiesSlice,
 | 
			
		||||
			timeout,
 | 
			
		||||
			&activeProxyClientsArray[i],
 | 
			
		||||
			increase,
 | 
			
		||||
			maxConns,
 | 
			
		||||
			URL,
 | 
			
		||||
			&wg,
 | 
			
		||||
		)
 | 
			
		||||
	}
 | 
			
		||||
	wg.Wait()
 | 
			
		||||
	streamCtxCancel()
 | 
			
		||||
	streamWG.Wait()
 | 
			
		||||
	return utils.Flatten(activeProxyClientsArray)
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
// findActiveProxyClients checks a list of proxies to determine which ones are active
 | 
			
		||||
// and appends the active ones to the provided activeProxyClients slice.
 | 
			
		||||
//
 | 
			
		||||
// Parameters:
 | 
			
		||||
//   - ctx: The context to control cancellation and timeout.
 | 
			
		||||
//   - proxies: A slice of Proxy configurations to be checked.
 | 
			
		||||
//   - timeout: The duration to wait for each proxy check before timing out.
 | 
			
		||||
//   - activeProxyClients: A pointer to a slice where active proxy clients will be appended.
 | 
			
		||||
//   - increase: A channel to signal the increase of checked proxies count.
 | 
			
		||||
//   - URL: The URL to be used for checking the proxies.
 | 
			
		||||
//   - wg: A WaitGroup to signal when the function is done.
 | 
			
		||||
//
 | 
			
		||||
// The function sends a GET request to each proxy using the provided URL. If the proxy
 | 
			
		||||
// responds with a status code of 200, it is considered active and added to the activeProxyClients slice.
 | 
			
		||||
// The function respects the context's cancellation and timeout settings.
 | 
			
		||||
func findActiveProxyClients(
 | 
			
		||||
	ctx context.Context,
 | 
			
		||||
	proxies []config.Proxy,
 | 
			
		||||
	timeout time.Duration,
 | 
			
		||||
	activeProxyClients *[]*fasthttp.HostClient,
 | 
			
		||||
	increase chan<- int64,
 | 
			
		||||
	maxConns uint,
 | 
			
		||||
	URL *url.URL,
 | 
			
		||||
	wg *sync.WaitGroup,
 | 
			
		||||
) {
 | 
			
		||||
	defer wg.Done()
 | 
			
		||||
 | 
			
		||||
	request := fasthttp.AcquireRequest()
 | 
			
		||||
	defer fasthttp.ReleaseRequest(request)
 | 
			
		||||
	request.SetRequestURI(config.ProxyCheckURL)
 | 
			
		||||
	request.Header.SetMethod("GET")
 | 
			
		||||
 | 
			
		||||
	for _, proxy := range proxies {
 | 
			
		||||
		if ctx.Err() != nil {
 | 
			
		||||
			return
 | 
			
		||||
		}
 | 
			
		||||
 | 
			
		||||
		func() {
 | 
			
		||||
			defer func() { increase <- 1 }()
 | 
			
		||||
 | 
			
		||||
			response := fasthttp.AcquireResponse()
 | 
			
		||||
			defer fasthttp.ReleaseResponse(response)
 | 
			
		||||
 | 
			
		||||
			dialFunc, err := getDialFunc(&proxy, timeout)
 | 
			
		||||
			if err != nil {
 | 
			
		||||
				return
 | 
			
		||||
			}
 | 
			
		||||
			client := &fasthttp.Client{
 | 
			
		||||
				Dial: dialFunc,
 | 
			
		||||
			}
 | 
			
		||||
			defer client.CloseIdleConnections()
 | 
			
		||||
 | 
			
		||||
			ch := make(chan error)
 | 
			
		||||
			go func() {
 | 
			
		||||
				err := client.DoTimeout(request, response, timeout)
 | 
			
		||||
				ch <- err
 | 
			
		||||
			}()
 | 
			
		||||
			select {
 | 
			
		||||
			case err := <-ch:
 | 
			
		||||
				if err != nil {
 | 
			
		||||
					return
 | 
			
		||||
				}
 | 
			
		||||
				break
 | 
			
		||||
			case <-time.After(timeout):
 | 
			
		||||
				return
 | 
			
		||||
			case <-ctx.Done():
 | 
			
		||||
				return
 | 
			
		||||
			}
 | 
			
		||||
 | 
			
		||||
			isTLS := URL.Scheme == "https"
 | 
			
		||||
			addr := URL.Host
 | 
			
		||||
			if isTLS && URL.Port() == "" {
 | 
			
		||||
				addr += ":443"
 | 
			
		||||
			}
 | 
			
		||||
			if response.StatusCode() == 200 {
 | 
			
		||||
				*activeProxyClients = append(
 | 
			
		||||
					*activeProxyClients,
 | 
			
		||||
					&fasthttp.HostClient{
 | 
			
		||||
						MaxConns:            int(maxConns),
 | 
			
		||||
						IsTLS:               isTLS,
 | 
			
		||||
						Addr:                addr,
 | 
			
		||||
						Dial:                dialFunc,
 | 
			
		||||
						MaxIdleConnDuration: timeout,
 | 
			
		||||
						MaxConnDuration:     timeout,
 | 
			
		||||
						WriteTimeout:        timeout,
 | 
			
		||||
						ReadTimeout:         timeout,
 | 
			
		||||
					},
 | 
			
		||||
				)
 | 
			
		||||
			}
 | 
			
		||||
		}()
 | 
			
		||||
	}
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
// getDialFunc returns a fasthttp.DialFunc based on the provided proxy configuration.
 | 
			
		||||
// It takes a pointer to a config.Proxy struct as input and returns a fasthttp.DialFunc and an error.
 | 
			
		||||
// The function parses the proxy URL, determines the scheme (socks5, socks5h, http, or https),
 | 
			
		||||
// and creates a dialer accordingly. If the proxy URL is invalid or the scheme is not supported,
 | 
			
		||||
// it returns an error.
 | 
			
		||||
func getDialFunc(proxy *config.Proxy, timeout time.Duration) (fasthttp.DialFunc, error) {
 | 
			
		||||
	parsedProxyURL, err := url.Parse(proxy.URL)
 | 
			
		||||
	if err != nil {
 | 
			
		||||
		return nil, err
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
// getDialFunc returns the appropriate fasthttp.DialFunc based on the provided proxy URL scheme.
 | 
			
		||||
// It supports SOCKS5 ('socks5' or 'socks5h') and HTTP ('http') proxy schemes.
 | 
			
		||||
// For HTTP proxies, the timeout parameter determines connection timeouts.
 | 
			
		||||
// Returns an error if the proxy scheme is unsupported.
 | 
			
		||||
func getDialFunc(proxy *url.URL, timeout time.Duration) (fasthttp.DialFunc, error) {
 | 
			
		||||
	var dialer fasthttp.DialFunc
 | 
			
		||||
	if parsedProxyURL.Scheme == "socks5" || parsedProxyURL.Scheme == "socks5h" {
 | 
			
		||||
		if proxy.Username != "" {
 | 
			
		||||
			dialer = fasthttpproxy.FasthttpSocksDialer(
 | 
			
		||||
				fmt.Sprintf(
 | 
			
		||||
					"%s://%s:%s@%s",
 | 
			
		||||
					parsedProxyURL.Scheme,
 | 
			
		||||
					proxy.Username,
 | 
			
		||||
					proxy.Password,
 | 
			
		||||
					parsedProxyURL.Host,
 | 
			
		||||
				),
 | 
			
		||||
			)
 | 
			
		||||
		} else {
 | 
			
		||||
			dialer = fasthttpproxy.FasthttpSocksDialer(
 | 
			
		||||
				fmt.Sprintf(
 | 
			
		||||
					"%s://%s",
 | 
			
		||||
					parsedProxyURL.Scheme,
 | 
			
		||||
					parsedProxyURL.Host,
 | 
			
		||||
				),
 | 
			
		||||
			)
 | 
			
		||||
 | 
			
		||||
	switch proxy.Scheme {
 | 
			
		||||
	case "socks5", "socks5h":
 | 
			
		||||
		dialer = fasthttpproxy.FasthttpSocksDialerDualStack(proxy.String())
 | 
			
		||||
	case "http":
 | 
			
		||||
		dialer = fasthttpproxy.FasthttpHTTPDialerDualStackTimeout(proxy.String(), timeout)
 | 
			
		||||
	default:
 | 
			
		||||
		return nil, errors.New("unsupported proxy scheme")
 | 
			
		||||
	}
 | 
			
		||||
	} else if parsedProxyURL.Scheme == "http" {
 | 
			
		||||
		if proxy.Username != "" {
 | 
			
		||||
			dialer = fasthttpproxy.FasthttpHTTPDialerTimeout(
 | 
			
		||||
				fmt.Sprintf(
 | 
			
		||||
					"%s:%s@%s",
 | 
			
		||||
					proxy.Username, proxy.Password, parsedProxyURL.Host,
 | 
			
		||||
				),
 | 
			
		||||
				timeout,
 | 
			
		||||
			)
 | 
			
		||||
		} else {
 | 
			
		||||
			dialer = fasthttpproxy.FasthttpHTTPDialerTimeout(
 | 
			
		||||
				parsedProxyURL.Host,
 | 
			
		||||
				timeout,
 | 
			
		||||
			)
 | 
			
		||||
		}
 | 
			
		||||
	} else {
 | 
			
		||||
		return nil, err
 | 
			
		||||
 | 
			
		||||
	if dialer == nil {
 | 
			
		||||
		return nil, errors.New("internal error: proxy dialer is nil")
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	return dialer, nil
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
 
 | 
			
		||||
@@ -7,7 +7,6 @@ import (
 | 
			
		||||
	"time"
 | 
			
		||||
 | 
			
		||||
	"github.com/jedib0t/go-pretty/v6/progress"
 | 
			
		||||
	"github.com/valyala/fasthttp"
 | 
			
		||||
)
 | 
			
		||||
 | 
			
		||||
// streamProgress streams the progress of a task to the console using a progress bar.
 | 
			
		||||
@@ -18,7 +17,7 @@ import (
 | 
			
		||||
func streamProgress(
 | 
			
		||||
	ctx context.Context,
 | 
			
		||||
	wg *sync.WaitGroup,
 | 
			
		||||
	total int64,
 | 
			
		||||
	total uint,
 | 
			
		||||
	message string,
 | 
			
		||||
	increase <-chan int64,
 | 
			
		||||
) {
 | 
			
		||||
@@ -28,19 +27,26 @@ func streamProgress(
 | 
			
		||||
	pw.SetStyle(progress.StyleBlocks)
 | 
			
		||||
	pw.SetTrackerLength(40)
 | 
			
		||||
	pw.SetUpdateFrequency(time.Millisecond * 250)
 | 
			
		||||
	if total == 0 {
 | 
			
		||||
		pw.Style().Visibility.Percentage = false
 | 
			
		||||
	}
 | 
			
		||||
	go pw.Render()
 | 
			
		||||
	dodosTracker := progress.Tracker{
 | 
			
		||||
		Message: message,
 | 
			
		||||
		Total:   total,
 | 
			
		||||
		Total:   int64(total),
 | 
			
		||||
	}
 | 
			
		||||
	pw.AppendTracker(&dodosTracker)
 | 
			
		||||
 | 
			
		||||
	for {
 | 
			
		||||
		select {
 | 
			
		||||
		case <-ctx.Done():
 | 
			
		||||
			fmt.Printf("\r")
 | 
			
		||||
			if err := ctx.Err(); err == context.Canceled || err == context.DeadlineExceeded {
 | 
			
		||||
				dodosTracker.MarkAsDone()
 | 
			
		||||
			} else {
 | 
			
		||||
				dodosTracker.MarkAsErrored()
 | 
			
		||||
			}
 | 
			
		||||
			time.Sleep(time.Millisecond * 300)
 | 
			
		||||
			pw.Stop()
 | 
			
		||||
			fmt.Printf("\r")
 | 
			
		||||
			return
 | 
			
		||||
 | 
			
		||||
		case value := <-increase:
 | 
			
		||||
@@ -48,28 +54,3 @@ func streamProgress(
 | 
			
		||||
		}
 | 
			
		||||
	}
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
// checkConnection checks the internet connection by making requests to different websites.
 | 
			
		||||
// It returns true if the connection is successful, otherwise false.
 | 
			
		||||
func checkConnection(ctx context.Context) bool {
 | 
			
		||||
	ch := make(chan bool)
 | 
			
		||||
	go func() {
 | 
			
		||||
		_, _, err := fasthttp.Get(nil, "https://www.google.com")
 | 
			
		||||
		if err != nil {
 | 
			
		||||
			_, _, err = fasthttp.Get(nil, "https://www.bing.com")
 | 
			
		||||
			if err != nil {
 | 
			
		||||
				_, _, err = fasthttp.Get(nil, "https://www.yahoo.com")
 | 
			
		||||
				ch <- err == nil
 | 
			
		||||
			}
 | 
			
		||||
			ch <- true
 | 
			
		||||
		}
 | 
			
		||||
		ch <- true
 | 
			
		||||
	}()
 | 
			
		||||
 | 
			
		||||
	select {
 | 
			
		||||
	case <-ctx.Done():
 | 
			
		||||
		return false
 | 
			
		||||
	case res := <-ch:
 | 
			
		||||
		return res
 | 
			
		||||
	}
 | 
			
		||||
}
 | 
			
		||||
 
 | 
			
		||||
@@ -1,13 +1,15 @@
 | 
			
		||||
package requests
 | 
			
		||||
 | 
			
		||||
import (
 | 
			
		||||
	"bytes"
 | 
			
		||||
	"context"
 | 
			
		||||
	"math/rand"
 | 
			
		||||
	"net/url"
 | 
			
		||||
	"text/template"
 | 
			
		||||
	"time"
 | 
			
		||||
 | 
			
		||||
	"github.com/aykhans/dodo/config"
 | 
			
		||||
	customerrors "github.com/aykhans/dodo/custom_errors"
 | 
			
		||||
	"github.com/aykhans/dodo/types"
 | 
			
		||||
	"github.com/aykhans/dodo/utils"
 | 
			
		||||
	"github.com/valyala/fasthttp"
 | 
			
		||||
)
 | 
			
		||||
@@ -21,12 +23,16 @@ type Request struct {
 | 
			
		||||
	getRequest RequestGeneratorFunc
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
type keyValueGenerator struct {
 | 
			
		||||
	key   func() string
 | 
			
		||||
	value func() string
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
// 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.
 | 
			
		||||
func (r *Request) Send(ctx context.Context, timeout time.Duration) (*fasthttp.Response, error) {
 | 
			
		||||
	client := r.getClient()
 | 
			
		||||
	request := r.getRequest()
 | 
			
		||||
	defer client.CloseIdleConnections()
 | 
			
		||||
	defer fasthttp.ReleaseRequest(request)
 | 
			
		||||
 | 
			
		||||
	response := fasthttp.AcquireResponse()
 | 
			
		||||
@@ -44,9 +50,9 @@ func (r *Request) Send(ctx context.Context, timeout time.Duration) (*fasthttp.Re
 | 
			
		||||
		return response, nil
 | 
			
		||||
	case <-time.After(timeout):
 | 
			
		||||
		fasthttp.ReleaseResponse(response)
 | 
			
		||||
		return nil, customerrors.ErrTimeout
 | 
			
		||||
		return nil, types.ErrTimeout
 | 
			
		||||
	case <-ctx.Done():
 | 
			
		||||
		return nil, customerrors.ErrInterrupt
 | 
			
		||||
		return nil, types.ErrInterrupt
 | 
			
		||||
	}
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
@@ -75,9 +81,9 @@ func newRequest(
 | 
			
		||||
 | 
			
		||||
	getRequest := getRequestGeneratorFunc(
 | 
			
		||||
		requestConfig.URL,
 | 
			
		||||
		requestConfig.Params,
 | 
			
		||||
		requestConfig.Headers,
 | 
			
		||||
		requestConfig.Cookies,
 | 
			
		||||
		requestConfig.Params,
 | 
			
		||||
		requestConfig.Method,
 | 
			
		||||
		requestConfig.Body,
 | 
			
		||||
		localRand,
 | 
			
		||||
@@ -91,38 +97,39 @@ func newRequest(
 | 
			
		||||
	return requests
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
// getRequestGeneratorFunc returns a RequestGeneratorFunc which generates HTTP requests
 | 
			
		||||
// with the specified parameters.
 | 
			
		||||
// The function uses a local random number generator to select bodies, headers, cookies, and parameters
 | 
			
		||||
// if multiple options are provided.
 | 
			
		||||
// getRequestGeneratorFunc returns a RequestGeneratorFunc which generates HTTP requests with the specified parameters.
 | 
			
		||||
// The function uses a local random number generator to select bodies, headers, cookies, and parameters if multiple options are provided.
 | 
			
		||||
func getRequestGeneratorFunc(
 | 
			
		||||
	URL *url.URL,
 | 
			
		||||
	Headers map[string][]string,
 | 
			
		||||
	Cookies map[string][]string,
 | 
			
		||||
	Params map[string][]string,
 | 
			
		||||
	Method string,
 | 
			
		||||
	Bodies []string,
 | 
			
		||||
	URL url.URL,
 | 
			
		||||
	params types.Params,
 | 
			
		||||
	headers types.Headers,
 | 
			
		||||
	cookies types.Cookies,
 | 
			
		||||
	method string,
 | 
			
		||||
	bodies []string,
 | 
			
		||||
	localRand *rand.Rand,
 | 
			
		||||
) 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)
 | 
			
		||||
	}
 | 
			
		||||
	getHeaders := getKeyValueSetFunc(Headers, localRand)
 | 
			
		||||
	getCookies := getKeyValueSetFunc(Cookies, localRand)
 | 
			
		||||
	getParams := getKeyValueSetFunc(Params, localRand)
 | 
			
		||||
	getParams := getKeyValueGeneratorFunc(params, localRand)
 | 
			
		||||
	getHeaders := getKeyValueGeneratorFunc(headers, localRand)
 | 
			
		||||
	getCookies := getKeyValueGeneratorFunc(cookies, localRand)
 | 
			
		||||
	getBody := getBodyValueFunc(bodies, utils.NewFuncMapGenerator(localRand), localRand)
 | 
			
		||||
 | 
			
		||||
	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(
 | 
			
		||||
			URL,
 | 
			
		||||
			getHeaders(),
 | 
			
		||||
			getCookies(),
 | 
			
		||||
			getParams(),
 | 
			
		||||
			Method,
 | 
			
		||||
			getBody(),
 | 
			
		||||
			headers,
 | 
			
		||||
			getCookies(),
 | 
			
		||||
			method,
 | 
			
		||||
			body,
 | 
			
		||||
		)
 | 
			
		||||
	}
 | 
			
		||||
}
 | 
			
		||||
@@ -130,12 +137,12 @@ func getRequestGeneratorFunc(
 | 
			
		||||
// newFasthttpRequest creates a new fasthttp.Request object with the provided parameters.
 | 
			
		||||
// It sets the request URI, host header, headers, cookies, params, method, and body.
 | 
			
		||||
func newFasthttpRequest(
 | 
			
		||||
	URL *url.URL,
 | 
			
		||||
	Headers map[string]string,
 | 
			
		||||
	Cookies map[string]string,
 | 
			
		||||
	Params map[string]string,
 | 
			
		||||
	Method string,
 | 
			
		||||
	Body string,
 | 
			
		||||
	URL url.URL,
 | 
			
		||||
	params []types.KeyValue[string, string],
 | 
			
		||||
	headers []types.KeyValue[string, string],
 | 
			
		||||
	cookies []types.KeyValue[string, string],
 | 
			
		||||
	method string,
 | 
			
		||||
	body string,
 | 
			
		||||
) *fasthttp.Request {
 | 
			
		||||
	request := fasthttp.AcquireRequest()
 | 
			
		||||
	request.SetRequestURI(URL.Path)
 | 
			
		||||
@@ -143,12 +150,12 @@ func newFasthttpRequest(
 | 
			
		||||
	// Set the host of the request to the host header
 | 
			
		||||
	// If the host header is not set, the request will fail
 | 
			
		||||
	// If there is host header in the headers, it will be overwritten
 | 
			
		||||
	request.Header.Set("Host", URL.Host)
 | 
			
		||||
	setRequestHeaders(request, Headers)
 | 
			
		||||
	setRequestCookies(request, Cookies)
 | 
			
		||||
	setRequestParams(request, Params)
 | 
			
		||||
	setRequestMethod(request, Method)
 | 
			
		||||
	setRequestBody(request, Body)
 | 
			
		||||
	request.Header.SetHost(URL.Host)
 | 
			
		||||
	setRequestParams(request, params)
 | 
			
		||||
	setRequestHeaders(request, headers)
 | 
			
		||||
	setRequestCookies(request, cookies)
 | 
			
		||||
	setRequestMethod(request, method)
 | 
			
		||||
	setRequestBody(request, body)
 | 
			
		||||
	if URL.Scheme == "https" {
 | 
			
		||||
		request.URI().SetScheme("https")
 | 
			
		||||
	}
 | 
			
		||||
@@ -156,28 +163,25 @@ func newFasthttpRequest(
 | 
			
		||||
	return request
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
// setRequestHeaders sets the headers of the given request with the provided key-value pairs.
 | 
			
		||||
func setRequestHeaders(req *fasthttp.Request, headers map[string]string) {
 | 
			
		||||
	req.Header.Set("User-Agent", config.DefaultUserAgent)
 | 
			
		||||
	for key, value := range headers {
 | 
			
		||||
		req.Header.Set(key, value)
 | 
			
		||||
// setRequestParams adds the query parameters of the given request based on the provided key-value pairs.
 | 
			
		||||
func setRequestParams(req *fasthttp.Request, params []types.KeyValue[string, string]) {
 | 
			
		||||
	for _, param := range params {
 | 
			
		||||
		req.URI().QueryArgs().Add(param.Key, param.Value)
 | 
			
		||||
	}
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
// setRequestCookies sets the cookies in the given request.
 | 
			
		||||
func setRequestCookies(req *fasthttp.Request, cookies map[string]string) {
 | 
			
		||||
	for key, value := range cookies {
 | 
			
		||||
		req.Header.SetCookie(key, value)
 | 
			
		||||
// setRequestHeaders adds the headers of the given request with the provided key-value pairs.
 | 
			
		||||
func setRequestHeaders(req *fasthttp.Request, headers []types.KeyValue[string, string]) {
 | 
			
		||||
	for _, header := range headers {
 | 
			
		||||
		req.Header.Add(header.Key, header.Value)
 | 
			
		||||
	}
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
// setRequestParams sets the query parameters of the given request based on the provided map of key-value pairs.
 | 
			
		||||
func setRequestParams(req *fasthttp.Request, params map[string]string) {
 | 
			
		||||
	urlParams := url.Values{}
 | 
			
		||||
	for key, value := range params {
 | 
			
		||||
		urlParams.Add(key, value)
 | 
			
		||||
// setRequestCookies adds the cookies of the given request with the provided key-value pairs.
 | 
			
		||||
func setRequestCookies(req *fasthttp.Request, cookies []types.KeyValue[string, string]) {
 | 
			
		||||
	for _, cookie := range cookies {
 | 
			
		||||
		req.Header.Add("Cookie", cookie.Key+"="+cookie.Value)
 | 
			
		||||
	}
 | 
			
		||||
	req.URI().SetQueryString(urlParams.Encode())
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
// setRequestMethod sets the HTTP request method for the given request.
 | 
			
		||||
@@ -191,59 +195,147 @@ func setRequestBody(req *fasthttp.Request, body string) {
 | 
			
		||||
	req.SetBody([]byte(body))
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
// getKeyValueSetFunc generates a function that returns a map of key-value pairs based on the provided key-value set.
 | 
			
		||||
// The generated function will either return fixed values or random values depending on the input.
 | 
			
		||||
// getKeyValueGeneratorFunc creates a function that generates key-value pairs for HTTP requests.
 | 
			
		||||
// It takes a slice of key-value pairs where each key maps to a slice of possible values,
 | 
			
		||||
// and a random number generator.
 | 
			
		||||
//
 | 
			
		||||
// Returns:
 | 
			
		||||
//   - A function that returns a map of key-value pairs. If the input map contains multiple values for a key,
 | 
			
		||||
//     the returned function will generate random values for that key. If the input map contains a single value
 | 
			
		||||
//     for a key, the returned function will always return that value. If the input map is empty for a key,
 | 
			
		||||
//     the returned function will generate an empty string for that key.
 | 
			
		||||
func getKeyValueSetFunc[
 | 
			
		||||
	KeyValueSet map[string][]string,
 | 
			
		||||
	KeyValue map[string]string,
 | 
			
		||||
](keyValueSet KeyValueSet, localRand *rand.Rand) func() KeyValue {
 | 
			
		||||
	getKeyValueSlice := []map[string]func() string{}
 | 
			
		||||
	isRandom := false
 | 
			
		||||
	for key, values := range keyValueSet {
 | 
			
		||||
		valuesLen := len(values)
 | 
			
		||||
// If any key has multiple possible values, the function will randomly select one value for each
 | 
			
		||||
// call (using the provided random number generator). If all keys have at most one value, the
 | 
			
		||||
// function will always return the same set of key-value pairs for efficiency.
 | 
			
		||||
func getKeyValueGeneratorFunc[
 | 
			
		||||
	T []types.KeyValue[string, string],
 | 
			
		||||
](
 | 
			
		||||
	keyValueSlice []types.KeyValue[string, []string],
 | 
			
		||||
	localRand *rand.Rand,
 | 
			
		||||
) func() T {
 | 
			
		||||
	keyValueGenerators := make([]keyValueGenerator, len(keyValueSlice))
 | 
			
		||||
 | 
			
		||||
		// if values is empty, return a function that generates empty string
 | 
			
		||||
		// if values has only one element, return a function that generates that element
 | 
			
		||||
		// if values has more than one element, return a function that generates a random element
 | 
			
		||||
		getKeyValue := func() string { return "" }
 | 
			
		||||
		if valuesLen == 1 {
 | 
			
		||||
			getKeyValue = func() string { return values[0] }
 | 
			
		||||
		} else if valuesLen > 1 {
 | 
			
		||||
			getKeyValue = utils.RandomValueCycle(values, localRand)
 | 
			
		||||
			isRandom = true
 | 
			
		||||
	funcMap := *utils.NewFuncMapGenerator(localRand).GetFuncMap()
 | 
			
		||||
 | 
			
		||||
	for i, kv := range keyValueSlice {
 | 
			
		||||
		keyValueGenerators[i] = keyValueGenerator{
 | 
			
		||||
			key:   getKeyFunc(kv.Key, funcMap),
 | 
			
		||||
			value: getValueFunc(kv.Value, funcMap, localRand),
 | 
			
		||||
		}
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
		getKeyValueSlice = append(
 | 
			
		||||
			getKeyValueSlice,
 | 
			
		||||
			map[string]func() string{key: getKeyValue},
 | 
			
		||||
		)
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	// if isRandom is true, return a function that generates random values,
 | 
			
		||||
	// otherwise return a function that generates fixed values to avoid unnecessary random number generation
 | 
			
		||||
	if isRandom {
 | 
			
		||||
		return func() KeyValue {
 | 
			
		||||
			keyValues := make(KeyValue, len(getKeyValueSlice))
 | 
			
		||||
			for _, keyValue := range getKeyValueSlice {
 | 
			
		||||
				for key, value := range keyValue {
 | 
			
		||||
					keyValues[key] = value()
 | 
			
		||||
	return func() T {
 | 
			
		||||
		keyValues := make(T, len(keyValueGenerators))
 | 
			
		||||
		for i, keyValue := range keyValueGenerators {
 | 
			
		||||
			keyValues[i] = types.KeyValue[string, string]{
 | 
			
		||||
				Key:   keyValue.key(),
 | 
			
		||||
				Value: keyValue.value(),
 | 
			
		||||
			}
 | 
			
		||||
		}
 | 
			
		||||
		return keyValues
 | 
			
		||||
	}
 | 
			
		||||
	} else {
 | 
			
		||||
		keyValues := make(KeyValue, len(getKeyValueSlice))
 | 
			
		||||
		for _, keyValue := range getKeyValueSlice {
 | 
			
		||||
			for key, value := range keyValue {
 | 
			
		||||
				keyValues[key] = value()
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
// 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()
 | 
			
		||||
		}
 | 
			
		||||
		}
 | 
			
		||||
		return func() KeyValue { return keyValues }
 | 
			
		||||
	}
 | 
			
		||||
}
 | 
			
		||||
 
 | 
			
		||||
@@ -4,7 +4,7 @@ import (
 | 
			
		||||
	"os"
 | 
			
		||||
	"time"
 | 
			
		||||
 | 
			
		||||
	. "github.com/aykhans/dodo/types"
 | 
			
		||||
	"github.com/aykhans/dodo/types"
 | 
			
		||||
	"github.com/aykhans/dodo/utils"
 | 
			
		||||
	"github.com/jedib0t/go-pretty/v6/table"
 | 
			
		||||
)
 | 
			
		||||
@@ -14,47 +14,30 @@ type Response struct {
 | 
			
		||||
	Time     time.Duration
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
type Responses []*Response
 | 
			
		||||
type Responses []Response
 | 
			
		||||
 | 
			
		||||
// Print prints the responses in a tabular format, including information such as
 | 
			
		||||
// response count, minimum time, maximum time, average time, and latency percentiles.
 | 
			
		||||
func (responses Responses) Print() {
 | 
			
		||||
	total := struct {
 | 
			
		||||
		Count int
 | 
			
		||||
		Min   time.Duration
 | 
			
		||||
		Max   time.Duration
 | 
			
		||||
		Sum   time.Duration
 | 
			
		||||
		P90   time.Duration
 | 
			
		||||
		P95   time.Duration
 | 
			
		||||
		P99   time.Duration
 | 
			
		||||
	}{
 | 
			
		||||
		Count: len(responses),
 | 
			
		||||
		Min:   responses[0].Time,
 | 
			
		||||
		Max:   responses[0].Time,
 | 
			
		||||
	if len(responses) == 0 {
 | 
			
		||||
		return
 | 
			
		||||
	}
 | 
			
		||||
	mergedResponses := make(map[string]Durations)
 | 
			
		||||
	var allDurations Durations
 | 
			
		||||
 | 
			
		||||
	for _, response := range responses {
 | 
			
		||||
		if response.Time < total.Min {
 | 
			
		||||
			total.Min = response.Time
 | 
			
		||||
		}
 | 
			
		||||
		if response.Time > total.Max {
 | 
			
		||||
			total.Max = response.Time
 | 
			
		||||
		}
 | 
			
		||||
		total.Sum += response.Time
 | 
			
		||||
	mergedResponses := make(map[string]types.Durations)
 | 
			
		||||
 | 
			
		||||
	totalDurations := make(types.Durations, len(responses))
 | 
			
		||||
	var totalSum time.Duration
 | 
			
		||||
	totalCount := len(responses)
 | 
			
		||||
 | 
			
		||||
	for i, response := range responses {
 | 
			
		||||
		totalSum += response.Time
 | 
			
		||||
		totalDurations[i] = response.Time
 | 
			
		||||
 | 
			
		||||
		mergedResponses[response.Response] = append(
 | 
			
		||||
			mergedResponses[response.Response],
 | 
			
		||||
			response.Time,
 | 
			
		||||
		)
 | 
			
		||||
		allDurations = append(allDurations, response.Time)
 | 
			
		||||
	}
 | 
			
		||||
	allDurations.Sort()
 | 
			
		||||
	allDurationsLenAsFloat := float64(len(allDurations) - 1)
 | 
			
		||||
	total.P90 = allDurations[int(0.90*allDurationsLenAsFloat)]
 | 
			
		||||
	total.P95 = allDurations[int(0.95*allDurationsLenAsFloat)]
 | 
			
		||||
	total.P99 = allDurations[int(0.99*allDurationsLenAsFloat)]
 | 
			
		||||
 | 
			
		||||
	t := table.NewWriter()
 | 
			
		||||
	t.SetOutputMirror(os.Stdout)
 | 
			
		||||
@@ -93,15 +76,18 @@ func (responses Responses) Print() {
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	if len(mergedResponses) > 1 {
 | 
			
		||||
		totalDurations.Sort()
 | 
			
		||||
		allDurationsLenAsFloat := float64(len(totalDurations) - 1)
 | 
			
		||||
 | 
			
		||||
		t.AppendRow(table.Row{
 | 
			
		||||
			"Total",
 | 
			
		||||
			total.Count,
 | 
			
		||||
			utils.DurationRoundBy(total.Min, roundPrecision),
 | 
			
		||||
			utils.DurationRoundBy(total.Max, roundPrecision),
 | 
			
		||||
			utils.DurationRoundBy(total.Sum/time.Duration(total.Count), roundPrecision), // Average
 | 
			
		||||
			utils.DurationRoundBy(total.P90, roundPrecision),
 | 
			
		||||
			utils.DurationRoundBy(total.P95, roundPrecision),
 | 
			
		||||
			utils.DurationRoundBy(total.P99, roundPrecision),
 | 
			
		||||
			totalCount,
 | 
			
		||||
			utils.DurationRoundBy(totalDurations[0], roundPrecision),
 | 
			
		||||
			utils.DurationRoundBy(totalDurations[len(totalDurations)-1], roundPrecision),
 | 
			
		||||
			utils.DurationRoundBy(totalSum/time.Duration(totalCount), roundPrecision), // Average
 | 
			
		||||
			utils.DurationRoundBy(totalDurations[int(0.90*allDurationsLenAsFloat)], roundPrecision),
 | 
			
		||||
			utils.DurationRoundBy(totalDurations[int(0.95*allDurationsLenAsFloat)], roundPrecision),
 | 
			
		||||
			utils.DurationRoundBy(totalDurations[int(0.99*allDurationsLenAsFloat)], roundPrecision),
 | 
			
		||||
		})
 | 
			
		||||
	}
 | 
			
		||||
	t.Render()
 | 
			
		||||
 
 | 
			
		||||
							
								
								
									
										123
									
								
								requests/run.go
									
									
									
									
									
								
							
							
						
						
									
										123
									
								
								requests/run.go
									
									
									
									
									
								
							@@ -7,48 +7,40 @@ import (
 | 
			
		||||
	"time"
 | 
			
		||||
 | 
			
		||||
	"github.com/aykhans/dodo/config"
 | 
			
		||||
	customerrors "github.com/aykhans/dodo/custom_errors"
 | 
			
		||||
	"github.com/aykhans/dodo/types"
 | 
			
		||||
	"github.com/aykhans/dodo/utils"
 | 
			
		||||
	"github.com/valyala/fasthttp"
 | 
			
		||||
)
 | 
			
		||||
 | 
			
		||||
// Run executes the main logic for processing requests based on the provided configuration.
 | 
			
		||||
// It first checks for an internet connection with a timeout context. If no connection is found,
 | 
			
		||||
// it returns an error. Then, it initializes clients based on the request configuration and
 | 
			
		||||
// releases the dodos. If the context is canceled and no responses are collected, it returns an interrupt error.
 | 
			
		||||
// It initializes clients based on the request configuration and releases the dodos.
 | 
			
		||||
// If the context is canceled and no responses are collected, it returns an interrupt error.
 | 
			
		||||
//
 | 
			
		||||
// Parameters:
 | 
			
		||||
//   - ctx: The context for managing request lifecycle and cancellation.
 | 
			
		||||
//   - requestConfig: The configuration for the request, including timeout, proxies, and other settings.
 | 
			
		||||
//
 | 
			
		||||
// Returns:
 | 
			
		||||
//   - Responses: A collection of responses from the executed requests.
 | 
			
		||||
//   - error: An error if the operation fails, such as no internet connection or an interrupt.
 | 
			
		||||
func Run(ctx context.Context, requestConfig *config.RequestConfig) (Responses, error) {
 | 
			
		||||
	checkConnectionCtx, checkConnectionCtxCancel := context.WithTimeout(ctx, 8*time.Second)
 | 
			
		||||
	if !checkConnection(checkConnectionCtx) {
 | 
			
		||||
		checkConnectionCtxCancel()
 | 
			
		||||
		return nil, customerrors.ErrNoInternet
 | 
			
		||||
	if requestConfig.Duration > 0 {
 | 
			
		||||
		var cancel context.CancelFunc
 | 
			
		||||
		ctx, cancel = context.WithTimeout(ctx, requestConfig.Duration)
 | 
			
		||||
		defer cancel()
 | 
			
		||||
	}
 | 
			
		||||
	checkConnectionCtxCancel()
 | 
			
		||||
 | 
			
		||||
	clients := getClients(
 | 
			
		||||
		ctx,
 | 
			
		||||
		requestConfig.Timeout,
 | 
			
		||||
		requestConfig.Proxies,
 | 
			
		||||
		requestConfig.GetValidDodosCountForProxies(),
 | 
			
		||||
		requestConfig.GetMaxConns(fasthttp.DefaultMaxConnsPerHost),
 | 
			
		||||
		requestConfig.Yes,
 | 
			
		||||
		requestConfig.NoProxyCheck,
 | 
			
		||||
		requestConfig.URL,
 | 
			
		||||
		requestConfig.SkipVerify,
 | 
			
		||||
	)
 | 
			
		||||
	if clients == nil {
 | 
			
		||||
		return nil, customerrors.ErrInterrupt
 | 
			
		||||
		return nil, types.ErrInterrupt
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	responses := releaseDodos(ctx, requestConfig, clients)
 | 
			
		||||
	if ctx.Err() != nil && len(responses) == 0 {
 | 
			
		||||
		return nil, customerrors.ErrInterrupt
 | 
			
		||||
		return nil, types.ErrInterrupt
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	return responses, nil
 | 
			
		||||
@@ -73,28 +65,38 @@ func releaseDodos(
 | 
			
		||||
		wg                  sync.WaitGroup
 | 
			
		||||
		streamWG            sync.WaitGroup
 | 
			
		||||
		requestCountPerDodo uint
 | 
			
		||||
		dodosCount          uint = requestConfig.GetValidDodosCountForRequests()
 | 
			
		||||
		dodosCountInt       int  = int(dodosCount)
 | 
			
		||||
		requestCount        uint = uint(requestConfig.RequestCount)
 | 
			
		||||
		responses                = make([][]*Response, dodosCount)
 | 
			
		||||
		increase                 = make(chan int64, requestCount)
 | 
			
		||||
		dodosCount          = requestConfig.GetValidDodosCountForRequests()
 | 
			
		||||
		responses           = make([][]Response, dodosCount)
 | 
			
		||||
		increase            = make(chan int64, requestConfig.RequestCount)
 | 
			
		||||
	)
 | 
			
		||||
 | 
			
		||||
	wg.Add(dodosCountInt)
 | 
			
		||||
	wg.Add(int(dodosCount))
 | 
			
		||||
	streamWG.Add(1)
 | 
			
		||||
	streamCtx, streamCtxCancel := context.WithCancel(context.Background())
 | 
			
		||||
	streamCtx, streamCtxCancel := context.WithCancel(ctx)
 | 
			
		||||
 | 
			
		||||
	go streamProgress(streamCtx, &streamWG, int64(requestCount), "Dodos Working🔥", increase)
 | 
			
		||||
	go streamProgress(streamCtx, &streamWG, requestConfig.RequestCount, "Dodos Working🔥", increase)
 | 
			
		||||
 | 
			
		||||
	if requestConfig.RequestCount == 0 {
 | 
			
		||||
		for i := range dodosCount {
 | 
			
		||||
			go sendRequest(
 | 
			
		||||
				ctx,
 | 
			
		||||
				newRequest(*requestConfig, clients, int64(i)),
 | 
			
		||||
				requestConfig.Timeout,
 | 
			
		||||
				&responses[i],
 | 
			
		||||
				increase,
 | 
			
		||||
				&wg,
 | 
			
		||||
			)
 | 
			
		||||
		}
 | 
			
		||||
	} else {
 | 
			
		||||
		for i := range dodosCount {
 | 
			
		||||
			if i+1 == dodosCount {
 | 
			
		||||
			requestCountPerDodo = requestCount - (i * requestCount / dodosCount)
 | 
			
		||||
				requestCountPerDodo = requestConfig.RequestCount - (i * requestConfig.RequestCount / dodosCount)
 | 
			
		||||
			} else {
 | 
			
		||||
			requestCountPerDodo = ((i + 1) * requestCount / dodosCount) -
 | 
			
		||||
				(i * requestCount / dodosCount)
 | 
			
		||||
				requestCountPerDodo = ((i + 1) * requestConfig.RequestCount / dodosCount) -
 | 
			
		||||
					(i * requestConfig.RequestCount / dodosCount)
 | 
			
		||||
			}
 | 
			
		||||
 | 
			
		||||
		go sendRequest(
 | 
			
		||||
			go sendRequestByCount(
 | 
			
		||||
				ctx,
 | 
			
		||||
				newRequest(*requestConfig, clients, int64(i)),
 | 
			
		||||
				requestConfig.Timeout,
 | 
			
		||||
@@ -104,22 +106,24 @@ func releaseDodos(
 | 
			
		||||
				&wg,
 | 
			
		||||
			)
 | 
			
		||||
		}
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	wg.Wait()
 | 
			
		||||
	streamCtxCancel()
 | 
			
		||||
	streamWG.Wait()
 | 
			
		||||
	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
 | 
			
		||||
// to the increase channel. The function terminates early if the context is canceled or if a custom
 | 
			
		||||
// interrupt error is encountered.
 | 
			
		||||
func sendRequest(
 | 
			
		||||
func sendRequestByCount(
 | 
			
		||||
	ctx context.Context,
 | 
			
		||||
	request *Request,
 | 
			
		||||
	timeout time.Duration,
 | 
			
		||||
	requestCount uint,
 | 
			
		||||
	responseData *[]*Response,
 | 
			
		||||
	responseData *[]Response,
 | 
			
		||||
	increase chan<- int64,
 | 
			
		||||
	wg *sync.WaitGroup,
 | 
			
		||||
) {
 | 
			
		||||
@@ -139,10 +143,10 @@ func sendRequest(
 | 
			
		||||
			}
 | 
			
		||||
 | 
			
		||||
			if err != nil {
 | 
			
		||||
				if err == customerrors.ErrInterrupt {
 | 
			
		||||
				if err == types.ErrInterrupt {
 | 
			
		||||
					return
 | 
			
		||||
				}
 | 
			
		||||
				*responseData = append(*responseData, &Response{
 | 
			
		||||
				*responseData = append(*responseData, Response{
 | 
			
		||||
					Response: err.Error(),
 | 
			
		||||
					Time:     completedTime,
 | 
			
		||||
				})
 | 
			
		||||
@@ -150,7 +154,54 @@ func sendRequest(
 | 
			
		||||
				return
 | 
			
		||||
			}
 | 
			
		||||
 | 
			
		||||
			*responseData = append(*responseData, &Response{
 | 
			
		||||
			*responseData = append(*responseData, Response{
 | 
			
		||||
				Response: strconv.Itoa(response.StatusCode()),
 | 
			
		||||
				Time:     completedTime,
 | 
			
		||||
			})
 | 
			
		||||
			increase <- 1
 | 
			
		||||
		}()
 | 
			
		||||
	}
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
// sendRequest continuously sends HTTP requests until the context is canceled.
 | 
			
		||||
// It records the response status code or error message along with the response time,
 | 
			
		||||
// and signals each completed request through the increase channel.
 | 
			
		||||
func sendRequest(
 | 
			
		||||
	ctx context.Context,
 | 
			
		||||
	request *Request,
 | 
			
		||||
	timeout time.Duration,
 | 
			
		||||
	responseData *[]Response,
 | 
			
		||||
	increase chan<- int64,
 | 
			
		||||
	wg *sync.WaitGroup,
 | 
			
		||||
) {
 | 
			
		||||
	defer wg.Done()
 | 
			
		||||
 | 
			
		||||
	for {
 | 
			
		||||
		if ctx.Err() != nil {
 | 
			
		||||
			return
 | 
			
		||||
		}
 | 
			
		||||
 | 
			
		||||
		func() {
 | 
			
		||||
			startTime := time.Now()
 | 
			
		||||
			response, err := request.Send(ctx, timeout)
 | 
			
		||||
			completedTime := time.Since(startTime)
 | 
			
		||||
			if response != nil {
 | 
			
		||||
				defer fasthttp.ReleaseResponse(response)
 | 
			
		||||
			}
 | 
			
		||||
 | 
			
		||||
			if err != nil {
 | 
			
		||||
				if err == types.ErrInterrupt {
 | 
			
		||||
					return
 | 
			
		||||
				}
 | 
			
		||||
				*responseData = append(*responseData, Response{
 | 
			
		||||
					Response: err.Error(),
 | 
			
		||||
					Time:     completedTime,
 | 
			
		||||
				})
 | 
			
		||||
				increase <- 1
 | 
			
		||||
				return
 | 
			
		||||
			}
 | 
			
		||||
 | 
			
		||||
			*responseData = append(*responseData, Response{
 | 
			
		||||
				Response: strconv.Itoa(response.StatusCode()),
 | 
			
		||||
				Time:     completedTime,
 | 
			
		||||
			})
 | 
			
		||||
 
 | 
			
		||||
							
								
								
									
										94
									
								
								types/body.go
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										94
									
								
								types/body.go
									
									
									
									
									
										Normal file
									
								
							@@ -0,0 +1,94 @@
 | 
			
		||||
package types
 | 
			
		||||
 | 
			
		||||
import (
 | 
			
		||||
	"bytes"
 | 
			
		||||
	"encoding/json"
 | 
			
		||||
	"fmt"
 | 
			
		||||
 | 
			
		||||
	"github.com/jedib0t/go-pretty/v6/text"
 | 
			
		||||
)
 | 
			
		||||
 | 
			
		||||
type Body []string
 | 
			
		||||
 | 
			
		||||
func (body Body) String() string {
 | 
			
		||||
	var buffer bytes.Buffer
 | 
			
		||||
	if len(body) == 0 {
 | 
			
		||||
		return buffer.String()
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	if len(body) == 1 {
 | 
			
		||||
		buffer.WriteString(body[0])
 | 
			
		||||
		return buffer.String()
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	buffer.WriteString(text.FgBlue.Sprint("Random") + "[\n")
 | 
			
		||||
 | 
			
		||||
	indent := "  "
 | 
			
		||||
 | 
			
		||||
	displayLimit := 5
 | 
			
		||||
 | 
			
		||||
	for i, item := range body[:min(len(body), displayLimit)] {
 | 
			
		||||
		if i > 0 {
 | 
			
		||||
			buffer.WriteString(",\n")
 | 
			
		||||
		}
 | 
			
		||||
 | 
			
		||||
		buffer.WriteString(indent + item)
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	// Add remaining count if there are more items
 | 
			
		||||
	if remainingValues := len(body) - displayLimit; remainingValues > 0 {
 | 
			
		||||
		buffer.WriteString(",\n" + indent + text.FgGreen.Sprintf("+%d bodies", remainingValues))
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	buffer.WriteString("\n]")
 | 
			
		||||
	return buffer.String()
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
func (body *Body) UnmarshalJSON(b []byte) error {
 | 
			
		||||
	var data any
 | 
			
		||||
	if err := json.Unmarshal(b, &data); err != nil {
 | 
			
		||||
		return err
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	switch v := data.(type) {
 | 
			
		||||
	case string:
 | 
			
		||||
		*body = []string{v}
 | 
			
		||||
	case []any:
 | 
			
		||||
		var slice []string
 | 
			
		||||
		for _, item := range v {
 | 
			
		||||
			slice = append(slice, fmt.Sprintf("%v", item))
 | 
			
		||||
		}
 | 
			
		||||
		*body = slice
 | 
			
		||||
	default:
 | 
			
		||||
		return fmt.Errorf("invalid type for Body: %T (should be string or []string)", v)
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	return nil
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
func (body *Body) UnmarshalYAML(unmarshal func(any) error) error {
 | 
			
		||||
	var data any
 | 
			
		||||
	if err := unmarshal(&data); err != nil {
 | 
			
		||||
		return err
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	switch v := data.(type) {
 | 
			
		||||
	case string:
 | 
			
		||||
		*body = []string{v}
 | 
			
		||||
	case []any:
 | 
			
		||||
		var slice []string
 | 
			
		||||
		for _, item := range v {
 | 
			
		||||
			slice = append(slice, fmt.Sprintf("%v", item))
 | 
			
		||||
		}
 | 
			
		||||
		*body = slice
 | 
			
		||||
	default:
 | 
			
		||||
		return fmt.Errorf("invalid type for Body: %T (should be string or []string)", v)
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	return nil
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
func (body *Body) Set(value string) error {
 | 
			
		||||
	*body = append(*body, value)
 | 
			
		||||
	return nil
 | 
			
		||||
}
 | 
			
		||||
							
								
								
									
										32
									
								
								types/config_file.go
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										32
									
								
								types/config_file.go
									
									
									
									
									
										Normal file
									
								
							@@ -0,0 +1,32 @@
 | 
			
		||||
package types
 | 
			
		||||
 | 
			
		||||
import "strings"
 | 
			
		||||
 | 
			
		||||
type FileLocationType int
 | 
			
		||||
 | 
			
		||||
const (
 | 
			
		||||
	FileLocationTypeLocal FileLocationType = iota
 | 
			
		||||
	FileLocationTypeRemoteHTTP
 | 
			
		||||
)
 | 
			
		||||
 | 
			
		||||
type ConfigFile string
 | 
			
		||||
 | 
			
		||||
func (configFile ConfigFile) String() string {
 | 
			
		||||
	return string(configFile)
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
func (configFile ConfigFile) LocationType() FileLocationType {
 | 
			
		||||
	if strings.HasPrefix(string(configFile), "http://") || strings.HasPrefix(string(configFile), "https://") {
 | 
			
		||||
		return FileLocationTypeRemoteHTTP
 | 
			
		||||
	}
 | 
			
		||||
	return FileLocationTypeLocal
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
func (configFile ConfigFile) Extension() string {
 | 
			
		||||
	i := strings.LastIndex(configFile.String(), ".")
 | 
			
		||||
	if i == -1 {
 | 
			
		||||
		return ""
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	return configFile.String()[i+1:]
 | 
			
		||||
}
 | 
			
		||||
							
								
								
									
										139
									
								
								types/cookies.go
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										139
									
								
								types/cookies.go
									
									
									
									
									
										Normal file
									
								
							@@ -0,0 +1,139 @@
 | 
			
		||||
package types
 | 
			
		||||
 | 
			
		||||
import (
 | 
			
		||||
	"bytes"
 | 
			
		||||
	"encoding/json"
 | 
			
		||||
	"fmt"
 | 
			
		||||
	"strings"
 | 
			
		||||
 | 
			
		||||
	"github.com/jedib0t/go-pretty/v6/text"
 | 
			
		||||
)
 | 
			
		||||
 | 
			
		||||
type Cookies []KeyValue[string, []string]
 | 
			
		||||
 | 
			
		||||
func (cookies Cookies) String() string {
 | 
			
		||||
	var buffer bytes.Buffer
 | 
			
		||||
	if len(cookies) == 0 {
 | 
			
		||||
		return buffer.String()
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	indent := "  "
 | 
			
		||||
 | 
			
		||||
	displayLimit := 3
 | 
			
		||||
 | 
			
		||||
	for i, item := range cookies[:min(len(cookies), displayLimit)] {
 | 
			
		||||
		if i > 0 {
 | 
			
		||||
			buffer.WriteString(",\n")
 | 
			
		||||
		}
 | 
			
		||||
 | 
			
		||||
		if len(item.Value) == 1 {
 | 
			
		||||
			buffer.WriteString(item.Key + ": " + item.Value[0])
 | 
			
		||||
			continue
 | 
			
		||||
		}
 | 
			
		||||
		buffer.WriteString(item.Key + ": " + text.FgBlue.Sprint("Random") + "[\n")
 | 
			
		||||
 | 
			
		||||
		for ii, v := range item.Value[:min(len(item.Value), displayLimit)] {
 | 
			
		||||
			if ii == len(item.Value)-1 {
 | 
			
		||||
				buffer.WriteString(indent + v + "\n")
 | 
			
		||||
			} else {
 | 
			
		||||
				buffer.WriteString(indent + v + ",\n")
 | 
			
		||||
			}
 | 
			
		||||
		}
 | 
			
		||||
 | 
			
		||||
		// Add remaining values count if needed
 | 
			
		||||
		if remainingValues := len(item.Value) - displayLimit; remainingValues > 0 {
 | 
			
		||||
			buffer.WriteString(indent + text.FgGreen.Sprintf("+%d values", remainingValues) + "\n")
 | 
			
		||||
		}
 | 
			
		||||
 | 
			
		||||
		buffer.WriteString("]")
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	// Add remaining key-value pairs count if needed
 | 
			
		||||
	if remainingPairs := len(cookies) - displayLimit; remainingPairs > 0 {
 | 
			
		||||
		buffer.WriteString(",\n" + text.FgGreen.Sprintf("+%d cookies", remainingPairs))
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	return buffer.String()
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
func (cookies *Cookies) AppendByKey(key, value string) {
 | 
			
		||||
	if item := cookies.GetValue(key); item != nil {
 | 
			
		||||
		*item = append(*item, value)
 | 
			
		||||
	} else {
 | 
			
		||||
		*cookies = append(*cookies, KeyValue[string, []string]{Key: key, Value: []string{value}})
 | 
			
		||||
	}
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
func (cookies Cookies) GetValue(key string) *[]string {
 | 
			
		||||
	for i := range cookies {
 | 
			
		||||
		if cookies[i].Key == key {
 | 
			
		||||
			return &cookies[i].Value
 | 
			
		||||
		}
 | 
			
		||||
	}
 | 
			
		||||
	return nil
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
func (cookies *Cookies) UnmarshalJSON(b []byte) error {
 | 
			
		||||
	var data []map[string]any
 | 
			
		||||
	if err := json.Unmarshal(b, &data); err != nil {
 | 
			
		||||
		return err
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	for _, item := range data {
 | 
			
		||||
		for key, value := range item {
 | 
			
		||||
			switch parsedValue := value.(type) {
 | 
			
		||||
			case string:
 | 
			
		||||
				*cookies = append(*cookies, KeyValue[string, []string]{Key: key, Value: []string{parsedValue}})
 | 
			
		||||
			case []any:
 | 
			
		||||
				parsedStr := make([]string, len(parsedValue))
 | 
			
		||||
				for i, item := range parsedValue {
 | 
			
		||||
					parsedStr[i] = fmt.Sprintf("%v", item)
 | 
			
		||||
				}
 | 
			
		||||
				*cookies = append(*cookies, KeyValue[string, []string]{Key: key, Value: parsedStr})
 | 
			
		||||
			default:
 | 
			
		||||
				return fmt.Errorf("unsupported type for cookies expected string or []string, got %T", parsedValue)
 | 
			
		||||
			}
 | 
			
		||||
		}
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	return nil
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
func (cookies *Cookies) UnmarshalYAML(unmarshal func(any) error) error {
 | 
			
		||||
	var raw []map[string]any
 | 
			
		||||
	if err := unmarshal(&raw); err != nil {
 | 
			
		||||
		return err
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	for _, param := range raw {
 | 
			
		||||
		for key, value := range param {
 | 
			
		||||
			switch parsed := value.(type) {
 | 
			
		||||
			case string:
 | 
			
		||||
				*cookies = append(*cookies, KeyValue[string, []string]{Key: key, Value: []string{parsed}})
 | 
			
		||||
			case []any:
 | 
			
		||||
				var values []string
 | 
			
		||||
				for _, v := range parsed {
 | 
			
		||||
					if str, ok := v.(string); ok {
 | 
			
		||||
						values = append(values, str)
 | 
			
		||||
					}
 | 
			
		||||
				}
 | 
			
		||||
				*cookies = append(*cookies, KeyValue[string, []string]{Key: key, Value: values})
 | 
			
		||||
			}
 | 
			
		||||
		}
 | 
			
		||||
	}
 | 
			
		||||
	return nil
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
func (cookies *Cookies) Set(value string) error {
 | 
			
		||||
	parts := strings.SplitN(value, "=", 2)
 | 
			
		||||
	switch len(parts) {
 | 
			
		||||
	case 0:
 | 
			
		||||
		cookies.AppendByKey("", "")
 | 
			
		||||
	case 1:
 | 
			
		||||
		cookies.AppendByKey(parts[0], "")
 | 
			
		||||
	case 2:
 | 
			
		||||
		cookies.AppendByKey(parts[0], parts[1])
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	return nil
 | 
			
		||||
}
 | 
			
		||||
							
								
								
									
										57
									
								
								types/duration.go
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										57
									
								
								types/duration.go
									
									
									
									
									
										Normal file
									
								
							@@ -0,0 +1,57 @@
 | 
			
		||||
package types
 | 
			
		||||
 | 
			
		||||
import (
 | 
			
		||||
	"encoding/json"
 | 
			
		||||
	"errors"
 | 
			
		||||
	"time"
 | 
			
		||||
)
 | 
			
		||||
 | 
			
		||||
type Duration struct {
 | 
			
		||||
	time.Duration
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
func (duration *Duration) UnmarshalJSON(b []byte) error {
 | 
			
		||||
	var v any
 | 
			
		||||
	if err := json.Unmarshal(b, &v); err != nil {
 | 
			
		||||
		return err
 | 
			
		||||
	}
 | 
			
		||||
	switch value := v.(type) {
 | 
			
		||||
	case float64:
 | 
			
		||||
		duration.Duration = time.Duration(value)
 | 
			
		||||
		return nil
 | 
			
		||||
	case string:
 | 
			
		||||
		var err error
 | 
			
		||||
		duration.Duration, err = time.ParseDuration(value)
 | 
			
		||||
		if err != nil {
 | 
			
		||||
			return errors.New("Duration is invalid (e.g. 400ms, 1s, 5m, 1h)")
 | 
			
		||||
		}
 | 
			
		||||
		return nil
 | 
			
		||||
	default:
 | 
			
		||||
		return errors.New("Duration is invalid (e.g. 400ms, 1s, 5m, 1h)")
 | 
			
		||||
	}
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
func (duration Duration) MarshalJSON() ([]byte, error) {
 | 
			
		||||
	return json.Marshal(duration.String())
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
func (duration *Duration) UnmarshalYAML(unmarshal func(any) error) error {
 | 
			
		||||
	var v any
 | 
			
		||||
	if err := unmarshal(&v); err != nil {
 | 
			
		||||
		return err
 | 
			
		||||
	}
 | 
			
		||||
	switch value := v.(type) {
 | 
			
		||||
	case float64:
 | 
			
		||||
		duration.Duration = time.Duration(value)
 | 
			
		||||
		return nil
 | 
			
		||||
	case string:
 | 
			
		||||
		var err error
 | 
			
		||||
		duration.Duration, err = time.ParseDuration(value)
 | 
			
		||||
		if err != nil {
 | 
			
		||||
			return errors.New("Duration is invalid (e.g. 400ms, 1s, 5m, 1h)")
 | 
			
		||||
		}
 | 
			
		||||
		return nil
 | 
			
		||||
	default:
 | 
			
		||||
		return errors.New("Duration is invalid (e.g. 400ms, 1s, 5m, 1h)")
 | 
			
		||||
	}
 | 
			
		||||
}
 | 
			
		||||
@@ -1,6 +1,7 @@
 | 
			
		||||
package types
 | 
			
		||||
 | 
			
		||||
import (
 | 
			
		||||
	"slices"
 | 
			
		||||
	"sort"
 | 
			
		||||
	"time"
 | 
			
		||||
)
 | 
			
		||||
@@ -14,9 +15,7 @@ func (d Durations) Sort(ascending ...bool) {
 | 
			
		||||
			return d[i] > d[j]
 | 
			
		||||
		})
 | 
			
		||||
	} else { // Otherwise, sort in ascending order
 | 
			
		||||
		sort.Slice(d, func(i, j int) bool {
 | 
			
		||||
			return d[i] < d[j]
 | 
			
		||||
		})
 | 
			
		||||
		slices.Sort(d)
 | 
			
		||||
	}
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
 
 | 
			
		||||
							
								
								
									
										10
									
								
								types/errors.go
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										10
									
								
								types/errors.go
									
									
									
									
									
										Normal file
									
								
							@@ -0,0 +1,10 @@
 | 
			
		||||
package types
 | 
			
		||||
 | 
			
		||||
import (
 | 
			
		||||
	"errors"
 | 
			
		||||
)
 | 
			
		||||
 | 
			
		||||
var (
 | 
			
		||||
	ErrInterrupt = errors.New("interrupted")
 | 
			
		||||
	ErrTimeout   = errors.New("timeout")
 | 
			
		||||
)
 | 
			
		||||
							
								
								
									
										156
									
								
								types/headers.go
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										156
									
								
								types/headers.go
									
									
									
									
									
										Normal file
									
								
							@@ -0,0 +1,156 @@
 | 
			
		||||
package types
 | 
			
		||||
 | 
			
		||||
import (
 | 
			
		||||
	"bytes"
 | 
			
		||||
	"encoding/json"
 | 
			
		||||
	"fmt"
 | 
			
		||||
	"strings"
 | 
			
		||||
 | 
			
		||||
	"github.com/jedib0t/go-pretty/v6/text"
 | 
			
		||||
)
 | 
			
		||||
 | 
			
		||||
type Headers []KeyValue[string, []string]
 | 
			
		||||
 | 
			
		||||
func (headers Headers) String() string {
 | 
			
		||||
	var buffer bytes.Buffer
 | 
			
		||||
	if len(headers) == 0 {
 | 
			
		||||
		return buffer.String()
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	indent := "  "
 | 
			
		||||
 | 
			
		||||
	displayLimit := 3
 | 
			
		||||
 | 
			
		||||
	for i, item := range headers[:min(len(headers), displayLimit)] {
 | 
			
		||||
		if i > 0 {
 | 
			
		||||
			buffer.WriteString(",\n")
 | 
			
		||||
		}
 | 
			
		||||
 | 
			
		||||
		if len(item.Value) == 1 {
 | 
			
		||||
			buffer.WriteString(item.Key + ": " + item.Value[0])
 | 
			
		||||
			continue
 | 
			
		||||
		}
 | 
			
		||||
		buffer.WriteString(item.Key + ": " + text.FgBlue.Sprint("Random") + "[\n")
 | 
			
		||||
 | 
			
		||||
		for ii, v := range item.Value[:min(len(item.Value), displayLimit)] {
 | 
			
		||||
			if ii == len(item.Value)-1 {
 | 
			
		||||
				buffer.WriteString(indent + v + "\n")
 | 
			
		||||
			} else {
 | 
			
		||||
				buffer.WriteString(indent + v + ",\n")
 | 
			
		||||
			}
 | 
			
		||||
		}
 | 
			
		||||
 | 
			
		||||
		// Add remaining values count if needed
 | 
			
		||||
		if remainingValues := len(item.Value) - displayLimit; remainingValues > 0 {
 | 
			
		||||
			buffer.WriteString(indent + text.FgGreen.Sprintf("+%d values", remainingValues) + "\n")
 | 
			
		||||
		}
 | 
			
		||||
 | 
			
		||||
		buffer.WriteString("]")
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	// Add remaining key-value pairs count if needed
 | 
			
		||||
	if remainingPairs := len(headers) - displayLimit; remainingPairs > 0 {
 | 
			
		||||
		buffer.WriteString(",\n" + text.FgGreen.Sprintf("+%d headers", remainingPairs))
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	return buffer.String()
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
func (headers *Headers) AppendByKey(key, value string) {
 | 
			
		||||
	if item := headers.GetValue(key); item != nil {
 | 
			
		||||
		*item = append(*item, value)
 | 
			
		||||
	} else {
 | 
			
		||||
		*headers = append(*headers, KeyValue[string, []string]{Key: key, Value: []string{value}})
 | 
			
		||||
	}
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
func (headers Headers) GetValue(key string) *[]string {
 | 
			
		||||
	for i := range headers {
 | 
			
		||||
		if headers[i].Key == key {
 | 
			
		||||
			return &headers[i].Value
 | 
			
		||||
		}
 | 
			
		||||
	}
 | 
			
		||||
	return nil
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
func (headers Headers) Has(key string) bool {
 | 
			
		||||
	for i := range headers {
 | 
			
		||||
		if headers[i].Key == key {
 | 
			
		||||
			return true
 | 
			
		||||
		}
 | 
			
		||||
	}
 | 
			
		||||
	return false
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
func (headers *Headers) UnmarshalJSON(b []byte) error {
 | 
			
		||||
	var data []map[string]any
 | 
			
		||||
	if err := json.Unmarshal(b, &data); err != nil {
 | 
			
		||||
		return err
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	for _, item := range data {
 | 
			
		||||
		for key, value := range item {
 | 
			
		||||
			switch parsedValue := value.(type) {
 | 
			
		||||
			case string:
 | 
			
		||||
				*headers = append(*headers, KeyValue[string, []string]{Key: key, Value: []string{parsedValue}})
 | 
			
		||||
			case []any:
 | 
			
		||||
				parsedStr := make([]string, len(parsedValue))
 | 
			
		||||
				for i, item := range parsedValue {
 | 
			
		||||
					parsedStr[i] = fmt.Sprintf("%v", item)
 | 
			
		||||
				}
 | 
			
		||||
				*headers = append(*headers, KeyValue[string, []string]{Key: key, Value: parsedStr})
 | 
			
		||||
			default:
 | 
			
		||||
				return fmt.Errorf("unsupported type for headers expected string or []string, got %T", parsedValue)
 | 
			
		||||
			}
 | 
			
		||||
		}
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	return nil
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
func (headers *Headers) UnmarshalYAML(unmarshal func(any) error) error {
 | 
			
		||||
	var raw []map[string]any
 | 
			
		||||
	if err := unmarshal(&raw); err != nil {
 | 
			
		||||
		return err
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	for _, param := range raw {
 | 
			
		||||
		for key, value := range param {
 | 
			
		||||
			switch parsed := value.(type) {
 | 
			
		||||
			case string:
 | 
			
		||||
				*headers = append(*headers, KeyValue[string, []string]{Key: key, Value: []string{parsed}})
 | 
			
		||||
			case []any:
 | 
			
		||||
				var values []string
 | 
			
		||||
				for _, v := range parsed {
 | 
			
		||||
					if str, ok := v.(string); ok {
 | 
			
		||||
						values = append(values, str)
 | 
			
		||||
					}
 | 
			
		||||
				}
 | 
			
		||||
				*headers = append(*headers, KeyValue[string, []string]{Key: key, Value: values})
 | 
			
		||||
			}
 | 
			
		||||
		}
 | 
			
		||||
	}
 | 
			
		||||
	return nil
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
func (headers *Headers) Set(value string) error {
 | 
			
		||||
	parts := strings.SplitN(value, ":", 2)
 | 
			
		||||
	switch len(parts) {
 | 
			
		||||
	case 0:
 | 
			
		||||
		headers.AppendByKey("", "")
 | 
			
		||||
	case 1:
 | 
			
		||||
		headers.AppendByKey(parts[0], "")
 | 
			
		||||
	case 2:
 | 
			
		||||
		headers.AppendByKey(parts[0], parts[1])
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	return nil
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
func (headers *Headers) SetIfNotExists(key string, value string) bool {
 | 
			
		||||
	if headers.Has(key) {
 | 
			
		||||
		return false
 | 
			
		||||
	}
 | 
			
		||||
	*headers = append(*headers, KeyValue[string, []string]{Key: key, Value: []string{value}})
 | 
			
		||||
	return true
 | 
			
		||||
}
 | 
			
		||||
							
								
								
									
										6
									
								
								types/key_value.go
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										6
									
								
								types/key_value.go
									
									
									
									
									
										Normal file
									
								
							@@ -0,0 +1,6 @@
 | 
			
		||||
package types
 | 
			
		||||
 | 
			
		||||
type KeyValue[K comparable, V any] struct {
 | 
			
		||||
	Key   K
 | 
			
		||||
	Value V
 | 
			
		||||
}
 | 
			
		||||
@@ -1,89 +0,0 @@
 | 
			
		||||
package types
 | 
			
		||||
 | 
			
		||||
import (
 | 
			
		||||
	"encoding/json"
 | 
			
		||||
	"errors"
 | 
			
		||||
)
 | 
			
		||||
 | 
			
		||||
type NonNilT interface {
 | 
			
		||||
	~int | ~float64 | ~string | ~bool
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
type Option[T NonNilT] interface {
 | 
			
		||||
	IsNone() bool
 | 
			
		||||
	ValueOrErr() (T, error)
 | 
			
		||||
	ValueOr(def T) T
 | 
			
		||||
	ValueOrPanic() T
 | 
			
		||||
	SetValue(value T)
 | 
			
		||||
	SetNone()
 | 
			
		||||
	UnmarshalJSON(data []byte) error
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
// Don't call this struct directly, use NewOption[T] or NewNoneOption[T] instead.
 | 
			
		||||
type option[T NonNilT] struct {
 | 
			
		||||
	// value holds the actual value of the Option if it is not None.
 | 
			
		||||
	value T
 | 
			
		||||
	// none indicates whether the Option is None (i.e., has no value).
 | 
			
		||||
	none bool
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
func (o *option[T]) IsNone() bool {
 | 
			
		||||
	return o.none
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
// If the Option is None, it will return zero value of the type and an error.
 | 
			
		||||
func (o *option[T]) ValueOrErr() (T, error) {
 | 
			
		||||
	if o.IsNone() {
 | 
			
		||||
		return o.value, errors.New("Option is None")
 | 
			
		||||
	}
 | 
			
		||||
	return o.value, nil
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
// If the Option is None, it will return the default value.
 | 
			
		||||
func (o *option[T]) ValueOr(def T) T {
 | 
			
		||||
	if o.IsNone() {
 | 
			
		||||
		return def
 | 
			
		||||
	}
 | 
			
		||||
	return o.value
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
// If the Option is None, it will panic.
 | 
			
		||||
func (o *option[T]) ValueOrPanic() T {
 | 
			
		||||
	if o.IsNone() {
 | 
			
		||||
		panic("Option is None")
 | 
			
		||||
	}
 | 
			
		||||
	return o.value
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
func (o *option[T]) SetValue(value T) {
 | 
			
		||||
	o.value = value
 | 
			
		||||
	o.none = false
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
func (o *option[T]) SetNone() {
 | 
			
		||||
	var zeroValue T
 | 
			
		||||
	o.value = zeroValue
 | 
			
		||||
	o.none = true
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
func (o *option[T]) UnmarshalJSON(data []byte) error {
 | 
			
		||||
	if string(data) == "null" || len(data) == 0 {
 | 
			
		||||
		o.SetNone()
 | 
			
		||||
		return nil
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	if err := json.Unmarshal(data, &o.value); err != nil {
 | 
			
		||||
		o.SetNone()
 | 
			
		||||
		return err
 | 
			
		||||
	}
 | 
			
		||||
	o.none = false
 | 
			
		||||
	return nil
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
func NewOption[T NonNilT](value T) *option[T] {
 | 
			
		||||
	return &option[T]{value: value}
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
func NewNoneOption[T NonNilT]() *option[T] {
 | 
			
		||||
	return &option[T]{none: true}
 | 
			
		||||
}
 | 
			
		||||
							
								
								
									
										139
									
								
								types/params.go
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										139
									
								
								types/params.go
									
									
									
									
									
										Normal file
									
								
							@@ -0,0 +1,139 @@
 | 
			
		||||
package types
 | 
			
		||||
 | 
			
		||||
import (
 | 
			
		||||
	"bytes"
 | 
			
		||||
	"encoding/json"
 | 
			
		||||
	"fmt"
 | 
			
		||||
	"strings"
 | 
			
		||||
 | 
			
		||||
	"github.com/jedib0t/go-pretty/v6/text"
 | 
			
		||||
)
 | 
			
		||||
 | 
			
		||||
type Params []KeyValue[string, []string]
 | 
			
		||||
 | 
			
		||||
func (params Params) String() string {
 | 
			
		||||
	var buffer bytes.Buffer
 | 
			
		||||
	if len(params) == 0 {
 | 
			
		||||
		return buffer.String()
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	indent := "  "
 | 
			
		||||
 | 
			
		||||
	displayLimit := 3
 | 
			
		||||
 | 
			
		||||
	for i, item := range params[:min(len(params), displayLimit)] {
 | 
			
		||||
		if i > 0 {
 | 
			
		||||
			buffer.WriteString(",\n")
 | 
			
		||||
		}
 | 
			
		||||
 | 
			
		||||
		if len(item.Value) == 1 {
 | 
			
		||||
			buffer.WriteString(item.Key + ": " + item.Value[0])
 | 
			
		||||
			continue
 | 
			
		||||
		}
 | 
			
		||||
		buffer.WriteString(item.Key + ": " + text.FgBlue.Sprint("Random") + "[\n")
 | 
			
		||||
 | 
			
		||||
		for ii, v := range item.Value[:min(len(item.Value), displayLimit)] {
 | 
			
		||||
			if ii == len(item.Value)-1 {
 | 
			
		||||
				buffer.WriteString(indent + v + "\n")
 | 
			
		||||
			} else {
 | 
			
		||||
				buffer.WriteString(indent + v + ",\n")
 | 
			
		||||
			}
 | 
			
		||||
		}
 | 
			
		||||
 | 
			
		||||
		// Add remaining values count if needed
 | 
			
		||||
		if remainingValues := len(item.Value) - displayLimit; remainingValues > 0 {
 | 
			
		||||
			buffer.WriteString(indent + text.FgGreen.Sprintf("+%d values", remainingValues) + "\n")
 | 
			
		||||
		}
 | 
			
		||||
 | 
			
		||||
		buffer.WriteString("]")
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	// Add remaining key-value pairs count if needed
 | 
			
		||||
	if remainingPairs := len(params) - displayLimit; remainingPairs > 0 {
 | 
			
		||||
		buffer.WriteString(",\n" + text.FgGreen.Sprintf("+%d params", remainingPairs))
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	return buffer.String()
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
func (params *Params) AppendByKey(key, value string) {
 | 
			
		||||
	if item := params.GetValue(key); item != nil {
 | 
			
		||||
		*item = append(*item, value)
 | 
			
		||||
	} else {
 | 
			
		||||
		*params = append(*params, KeyValue[string, []string]{Key: key, Value: []string{value}})
 | 
			
		||||
	}
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
func (params Params) GetValue(key string) *[]string {
 | 
			
		||||
	for i := range params {
 | 
			
		||||
		if params[i].Key == key {
 | 
			
		||||
			return ¶ms[i].Value
 | 
			
		||||
		}
 | 
			
		||||
	}
 | 
			
		||||
	return nil
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
func (params *Params) UnmarshalJSON(b []byte) error {
 | 
			
		||||
	var data []map[string]any
 | 
			
		||||
	if err := json.Unmarshal(b, &data); err != nil {
 | 
			
		||||
		return err
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	for _, item := range data {
 | 
			
		||||
		for key, value := range item {
 | 
			
		||||
			switch parsedValue := value.(type) {
 | 
			
		||||
			case string:
 | 
			
		||||
				*params = append(*params, KeyValue[string, []string]{Key: key, Value: []string{parsedValue}})
 | 
			
		||||
			case []any:
 | 
			
		||||
				parsedStr := make([]string, len(parsedValue))
 | 
			
		||||
				for i, item := range parsedValue {
 | 
			
		||||
					parsedStr[i] = fmt.Sprintf("%v", item)
 | 
			
		||||
				}
 | 
			
		||||
				*params = append(*params, KeyValue[string, []string]{Key: key, Value: parsedStr})
 | 
			
		||||
			default:
 | 
			
		||||
				return fmt.Errorf("unsupported type for params expected string or []string, got %T", parsedValue)
 | 
			
		||||
			}
 | 
			
		||||
		}
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	return nil
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
func (params *Params) UnmarshalYAML(unmarshal func(any) error) error {
 | 
			
		||||
	var raw []map[string]any
 | 
			
		||||
	if err := unmarshal(&raw); err != nil {
 | 
			
		||||
		return err
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	for _, param := range raw {
 | 
			
		||||
		for key, value := range param {
 | 
			
		||||
			switch parsed := value.(type) {
 | 
			
		||||
			case string:
 | 
			
		||||
				*params = append(*params, KeyValue[string, []string]{Key: key, Value: []string{parsed}})
 | 
			
		||||
			case []any:
 | 
			
		||||
				var values []string
 | 
			
		||||
				for _, v := range parsed {
 | 
			
		||||
					if str, ok := v.(string); ok {
 | 
			
		||||
						values = append(values, str)
 | 
			
		||||
					}
 | 
			
		||||
				}
 | 
			
		||||
				*params = append(*params, KeyValue[string, []string]{Key: key, Value: values})
 | 
			
		||||
			}
 | 
			
		||||
		}
 | 
			
		||||
	}
 | 
			
		||||
	return nil
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
func (params *Params) Set(value string) error {
 | 
			
		||||
	parts := strings.SplitN(value, "=", 2)
 | 
			
		||||
	switch len(parts) {
 | 
			
		||||
	case 0:
 | 
			
		||||
		params.AppendByKey("", "")
 | 
			
		||||
	case 1:
 | 
			
		||||
		params.AppendByKey(parts[0], "")
 | 
			
		||||
	case 2:
 | 
			
		||||
		params.AppendByKey(parts[0], parts[1])
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	return nil
 | 
			
		||||
}
 | 
			
		||||
							
								
								
									
										116
									
								
								types/proxies.go
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										116
									
								
								types/proxies.go
									
									
									
									
									
										Normal file
									
								
							@@ -0,0 +1,116 @@
 | 
			
		||||
package types
 | 
			
		||||
 | 
			
		||||
import (
 | 
			
		||||
	"bytes"
 | 
			
		||||
	"encoding/json"
 | 
			
		||||
	"fmt"
 | 
			
		||||
	"net/url"
 | 
			
		||||
 | 
			
		||||
	"github.com/jedib0t/go-pretty/v6/text"
 | 
			
		||||
)
 | 
			
		||||
 | 
			
		||||
type Proxies []url.URL
 | 
			
		||||
 | 
			
		||||
func (proxies Proxies) String() string {
 | 
			
		||||
	var buffer bytes.Buffer
 | 
			
		||||
	if len(proxies) == 0 {
 | 
			
		||||
		return buffer.String()
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	if len(proxies) == 1 {
 | 
			
		||||
		buffer.WriteString(proxies[0].String())
 | 
			
		||||
		return buffer.String()
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	buffer.WriteString(text.FgBlue.Sprint("Random") + "[\n")
 | 
			
		||||
 | 
			
		||||
	indent := "  "
 | 
			
		||||
 | 
			
		||||
	displayLimit := 5
 | 
			
		||||
 | 
			
		||||
	for i, item := range proxies[:min(len(proxies), displayLimit)] {
 | 
			
		||||
		if i > 0 {
 | 
			
		||||
			buffer.WriteString(",\n")
 | 
			
		||||
		}
 | 
			
		||||
 | 
			
		||||
		buffer.WriteString(indent + item.String())
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	// Add remaining count if there are more items
 | 
			
		||||
	if remainingValues := len(proxies) - displayLimit; remainingValues > 0 {
 | 
			
		||||
		buffer.WriteString(",\n" + indent + text.FgGreen.Sprintf("+%d proxies", remainingValues))
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	buffer.WriteString("\n]")
 | 
			
		||||
	return buffer.String()
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
func (proxies *Proxies) UnmarshalJSON(b []byte) error {
 | 
			
		||||
	var data any
 | 
			
		||||
	if err := json.Unmarshal(b, &data); err != nil {
 | 
			
		||||
		return err
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	switch v := data.(type) {
 | 
			
		||||
	case string:
 | 
			
		||||
		parsed, err := url.Parse(v)
 | 
			
		||||
		if err != nil {
 | 
			
		||||
			return err
 | 
			
		||||
		}
 | 
			
		||||
		*proxies = []url.URL{*parsed}
 | 
			
		||||
	case []any:
 | 
			
		||||
		var urls []url.URL
 | 
			
		||||
		for _, item := range v {
 | 
			
		||||
			url, err := url.Parse(item.(string))
 | 
			
		||||
			if err != nil {
 | 
			
		||||
				return err
 | 
			
		||||
			}
 | 
			
		||||
			urls = append(urls, *url)
 | 
			
		||||
		}
 | 
			
		||||
		*proxies = urls
 | 
			
		||||
	default:
 | 
			
		||||
		return fmt.Errorf("invalid type for Body: %T (should be URL or []URL)", v)
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	return nil
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
func (proxies *Proxies) UnmarshalYAML(unmarshal func(any) error) error {
 | 
			
		||||
	var data any
 | 
			
		||||
	if err := unmarshal(&data); err != nil {
 | 
			
		||||
		return err
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	switch v := data.(type) {
 | 
			
		||||
	case string:
 | 
			
		||||
		parsed, err := url.Parse(v)
 | 
			
		||||
		if err != nil {
 | 
			
		||||
			return err
 | 
			
		||||
		}
 | 
			
		||||
		*proxies = []url.URL{*parsed}
 | 
			
		||||
	case []any:
 | 
			
		||||
		var urls []url.URL
 | 
			
		||||
		for _, item := range v {
 | 
			
		||||
			url, err := url.Parse(item.(string))
 | 
			
		||||
			if err != nil {
 | 
			
		||||
				return err
 | 
			
		||||
			}
 | 
			
		||||
			urls = append(urls, *url)
 | 
			
		||||
		}
 | 
			
		||||
		*proxies = urls
 | 
			
		||||
	default:
 | 
			
		||||
		return fmt.Errorf("invalid type for Body: %T (should be URL or []URL)", v)
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	return nil
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
func (proxies *Proxies) Set(value string) error {
 | 
			
		||||
	parsedURL, err := url.Parse(value)
 | 
			
		||||
	if err != nil {
 | 
			
		||||
		return err
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	*proxies = append(*proxies, *parsedURL)
 | 
			
		||||
	return nil
 | 
			
		||||
}
 | 
			
		||||
							
								
								
									
										59
									
								
								types/request_url.go
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										59
									
								
								types/request_url.go
									
									
									
									
									
										Normal file
									
								
							@@ -0,0 +1,59 @@
 | 
			
		||||
package types
 | 
			
		||||
 | 
			
		||||
import (
 | 
			
		||||
	"encoding/json"
 | 
			
		||||
	"errors"
 | 
			
		||||
	"net/url"
 | 
			
		||||
)
 | 
			
		||||
 | 
			
		||||
type RequestURL struct {
 | 
			
		||||
	url.URL
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
func (requestURL *RequestURL) UnmarshalJSON(data []byte) error {
 | 
			
		||||
	var urlStr string
 | 
			
		||||
	if err := json.Unmarshal(data, &urlStr); err != nil {
 | 
			
		||||
		return err
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	parsedURL, err := url.Parse(urlStr)
 | 
			
		||||
	if err != nil {
 | 
			
		||||
		return errors.New("request URL is invalid")
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	requestURL.URL = *parsedURL
 | 
			
		||||
	return nil
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
func (requestURL *RequestURL) UnmarshalYAML(unmarshal func(any) error) error {
 | 
			
		||||
	var urlStr string
 | 
			
		||||
	if err := unmarshal(&urlStr); err != nil {
 | 
			
		||||
		return err
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	parsedURL, err := url.Parse(urlStr)
 | 
			
		||||
	if err != nil {
 | 
			
		||||
		return errors.New("request URL is invalid")
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	requestURL.URL = *parsedURL
 | 
			
		||||
	return nil
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
func (requestURL RequestURL) MarshalJSON() ([]byte, error) {
 | 
			
		||||
	return json.Marshal(requestURL.URL.String())
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
func (requestURL RequestURL) String() string {
 | 
			
		||||
	return requestURL.URL.String()
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
func (requestURL *RequestURL) Set(value string) error {
 | 
			
		||||
	parsedURL, err := url.Parse(value)
 | 
			
		||||
	if err != nil {
 | 
			
		||||
		return err
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	requestURL.URL = *parsedURL
 | 
			
		||||
	return nil
 | 
			
		||||
}
 | 
			
		||||
							
								
								
									
										57
									
								
								types/timeout.go
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										57
									
								
								types/timeout.go
									
									
									
									
									
										Normal 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)")
 | 
			
		||||
	}
 | 
			
		||||
}
 | 
			
		||||
							
								
								
									
										10
									
								
								utils/compare.go
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										10
									
								
								utils/compare.go
									
									
									
									
									
										Normal file
									
								
							@@ -0,0 +1,10 @@
 | 
			
		||||
package utils
 | 
			
		||||
 | 
			
		||||
func IsNilOrZero[T comparable](value *T) bool {
 | 
			
		||||
	if value == nil {
 | 
			
		||||
		return true
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	var zero T
 | 
			
		||||
	return *value == zero
 | 
			
		||||
}
 | 
			
		||||
@@ -1,82 +1,5 @@
 | 
			
		||||
package utils
 | 
			
		||||
 | 
			
		||||
import (
 | 
			
		||||
	"encoding/json"
 | 
			
		||||
	"fmt"
 | 
			
		||||
	"reflect"
 | 
			
		||||
)
 | 
			
		||||
 | 
			
		||||
type TruncatedMarshaller struct {
 | 
			
		||||
	Value    interface{}
 | 
			
		||||
	MaxItems int
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
func (t TruncatedMarshaller) MarshalJSON() ([]byte, error) {
 | 
			
		||||
	val := reflect.ValueOf(t.Value)
 | 
			
		||||
 | 
			
		||||
	if val.Kind() != reflect.Slice && val.Kind() != reflect.Array {
 | 
			
		||||
		return json.Marshal(t.Value)
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	length := val.Len()
 | 
			
		||||
	if length <= t.MaxItems {
 | 
			
		||||
		return json.Marshal(t.Value)
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	truncated := make([]interface{}, t.MaxItems+1)
 | 
			
		||||
 | 
			
		||||
	for i := 0; i < t.MaxItems; i++ {
 | 
			
		||||
		truncated[i] = val.Index(i).Interface()
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	remaining := length - t.MaxItems
 | 
			
		||||
	truncated[t.MaxItems] = fmt.Sprintf("+%d", remaining)
 | 
			
		||||
 | 
			
		||||
	return json.Marshal(truncated)
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
func PrettyJSONMarshal(v interface{}, maxItems int, prefix, indent string) []byte {
 | 
			
		||||
	truncated := processValue(v, maxItems)
 | 
			
		||||
	d, _ := json.MarshalIndent(truncated, prefix, indent)
 | 
			
		||||
	return d
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
func processValue(v interface{}, maxItems int) interface{} {
 | 
			
		||||
	val := reflect.ValueOf(v)
 | 
			
		||||
 | 
			
		||||
	switch val.Kind() {
 | 
			
		||||
	case reflect.Map:
 | 
			
		||||
		newMap := make(map[string]interface{})
 | 
			
		||||
		iter := val.MapRange()
 | 
			
		||||
		for iter.Next() {
 | 
			
		||||
			k := iter.Key().String()
 | 
			
		||||
			newMap[k] = processValue(iter.Value().Interface(), maxItems)
 | 
			
		||||
		}
 | 
			
		||||
		return newMap
 | 
			
		||||
 | 
			
		||||
	case reflect.Slice, reflect.Array:
 | 
			
		||||
		return TruncatedMarshaller{Value: v, MaxItems: maxItems}
 | 
			
		||||
 | 
			
		||||
	case reflect.Struct:
 | 
			
		||||
		newMap := make(map[string]interface{})
 | 
			
		||||
		t := val.Type()
 | 
			
		||||
		for i := 0; i < t.NumField(); i++ {
 | 
			
		||||
			field := t.Field(i)
 | 
			
		||||
			if field.IsExported() {
 | 
			
		||||
				jsonTag := field.Tag.Get("json")
 | 
			
		||||
				if jsonTag == "-" {
 | 
			
		||||
					continue
 | 
			
		||||
				}
 | 
			
		||||
				fieldName := field.Name
 | 
			
		||||
				if jsonTag != "" {
 | 
			
		||||
					fieldName = jsonTag
 | 
			
		||||
				}
 | 
			
		||||
				newMap[fieldName] = processValue(val.Field(i).Interface(), maxItems)
 | 
			
		||||
			}
 | 
			
		||||
		}
 | 
			
		||||
		return newMap
 | 
			
		||||
 | 
			
		||||
	default:
 | 
			
		||||
		return v
 | 
			
		||||
	}
 | 
			
		||||
func ToPtr[T any](value T) *T {
 | 
			
		||||
	return &value
 | 
			
		||||
}
 | 
			
		||||
 
 | 
			
		||||
@@ -3,46 +3,12 @@ package utils
 | 
			
		||||
import (
 | 
			
		||||
	"fmt"
 | 
			
		||||
	"os"
 | 
			
		||||
 | 
			
		||||
	"github.com/jedib0t/go-pretty/v6/text"
 | 
			
		||||
)
 | 
			
		||||
 | 
			
		||||
var Colors = struct {
 | 
			
		||||
	reset   string
 | 
			
		||||
	Red     string
 | 
			
		||||
	Green   string
 | 
			
		||||
	Yellow  string
 | 
			
		||||
	Orange  string
 | 
			
		||||
	Blue    string
 | 
			
		||||
	Magenta string
 | 
			
		||||
	Cyan    string
 | 
			
		||||
	Gray    string
 | 
			
		||||
	White   string
 | 
			
		||||
}{
 | 
			
		||||
	reset:   "\033[0m",
 | 
			
		||||
	Red:     "\033[31m",
 | 
			
		||||
	Green:   "\033[32m",
 | 
			
		||||
	Yellow:  "\033[33m",
 | 
			
		||||
	Orange:  "\033[38;5;208m",
 | 
			
		||||
	Blue:    "\033[34m",
 | 
			
		||||
	Magenta: "\033[35m",
 | 
			
		||||
	Cyan:    "\033[36m",
 | 
			
		||||
	Gray:    "\033[37m",
 | 
			
		||||
	White:   "\033[97m",
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
func Colored(color string, a ...any) string {
 | 
			
		||||
	return color + fmt.Sprint(a...) + Colors.reset
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
func PrintfC(color string, format string, a ...any) {
 | 
			
		||||
	fmt.Printf(Colored(color, format), a...)
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
func PrintlnC(color string, a ...any) {
 | 
			
		||||
	fmt.Println(Colored(color, a...))
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
func PrintErr(err error) {
 | 
			
		||||
	PrintlnC(Colors.Red, err.Error())
 | 
			
		||||
	fmt.Fprintln(os.Stderr, text.FgRed.Sprint(err.Error()))
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
func PrintErrAndExit(err error) {
 | 
			
		||||
 
 | 
			
		||||
@@ -2,49 +2,41 @@ package utils
 | 
			
		||||
 | 
			
		||||
import "math/rand"
 | 
			
		||||
 | 
			
		||||
func Flatten[T any](nested [][]*T) []*T {
 | 
			
		||||
	flattened := make([]*T, 0)
 | 
			
		||||
func Flatten[T any](nested [][]T) []T {
 | 
			
		||||
	flattened := make([]T, 0)
 | 
			
		||||
	for _, n := range nested {
 | 
			
		||||
		flattened = append(flattened, n...)
 | 
			
		||||
	}
 | 
			
		||||
	return flattened
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
func Contains[T comparable](slice []T, item T) bool {
 | 
			
		||||
	for _, i := range slice {
 | 
			
		||||
		if i == item {
 | 
			
		||||
			return true
 | 
			
		||||
		}
 | 
			
		||||
	}
 | 
			
		||||
	return false
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
// RandomValueCycle returns a function that cycles through the provided slice of values
 | 
			
		||||
// in a random order. Each call to the returned function will yield a value from the slice.
 | 
			
		||||
// The order of values is determined by the provided random number generator.
 | 
			
		||||
//
 | 
			
		||||
// The returned function will cycle through the values in a random order until all values
 | 
			
		||||
// have been returned at least once. After all values have been returned, the function will
 | 
			
		||||
// reset and start cycling through the values in a random order again.
 | 
			
		||||
// The returned function isn't thread-safe and should be used in a single-threaded context.
 | 
			
		||||
func RandomValueCycle[Value any](values []Value, localRand *rand.Rand) func() Value {
 | 
			
		||||
	var (
 | 
			
		||||
		clientsCount int = len(values)
 | 
			
		||||
		currentIndex int = localRand.Intn(clientsCount)
 | 
			
		||||
		stopIndex    int = currentIndex
 | 
			
		||||
	)
 | 
			
		||||
 | 
			
		||||
	return func() Value {
 | 
			
		||||
		client := values[currentIndex]
 | 
			
		||||
// RandomValueCycle returns a function that cycles through the provided values in a pseudo-random order.
 | 
			
		||||
// Each value in the input slice will be returned before any value is repeated.
 | 
			
		||||
// 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.
 | 
			
		||||
// This function is not thread-safe and should not be called concurrently.
 | 
			
		||||
func RandomValueCycle[T any](values []T, localRand *rand.Rand) func() T {
 | 
			
		||||
	switch valuesLen := len(values); valuesLen {
 | 
			
		||||
	case 0:
 | 
			
		||||
		var zero T
 | 
			
		||||
		return func() T { return zero }
 | 
			
		||||
	case 1:
 | 
			
		||||
		return func() T { return values[0] }
 | 
			
		||||
	default:
 | 
			
		||||
		currentIndex := localRand.Intn(valuesLen)
 | 
			
		||||
		stopIndex := currentIndex
 | 
			
		||||
		return func() T {
 | 
			
		||||
			value := values[currentIndex]
 | 
			
		||||
			currentIndex++
 | 
			
		||||
		if currentIndex == clientsCount {
 | 
			
		||||
			if currentIndex == valuesLen {
 | 
			
		||||
				currentIndex = 0
 | 
			
		||||
			}
 | 
			
		||||
			if currentIndex == stopIndex {
 | 
			
		||||
			currentIndex = localRand.Intn(clientsCount)
 | 
			
		||||
				currentIndex = localRand.Intn(valuesLen)
 | 
			
		||||
				stopIndex = currentIndex
 | 
			
		||||
			}
 | 
			
		||||
 | 
			
		||||
		return client
 | 
			
		||||
			return value
 | 
			
		||||
		}
 | 
			
		||||
	}
 | 
			
		||||
}
 | 
			
		||||
 
 | 
			
		||||
							
								
								
									
										479
									
								
								utils/templates.go
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										479
									
								
								utils/templates.go
									
									
									
									
									
										Normal 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,
 | 
			
		||||
	}
 | 
			
		||||
}
 | 
			
		||||
@@ -1,59 +0,0 @@
 | 
			
		||||
package validation
 | 
			
		||||
 | 
			
		||||
import (
 | 
			
		||||
	"reflect"
 | 
			
		||||
	"strings"
 | 
			
		||||
 | 
			
		||||
	"github.com/go-playground/validator/v10"
 | 
			
		||||
	"golang.org/x/net/http/httpguts"
 | 
			
		||||
)
 | 
			
		||||
 | 
			
		||||
// net/http/request.go/isNotToken
 | 
			
		||||
func isNotToken(r rune) bool {
 | 
			
		||||
	return !httpguts.IsTokenRune(r)
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
func NewValidator() *validator.Validate {
 | 
			
		||||
	validation := validator.New()
 | 
			
		||||
	validation.RegisterTagNameFunc(func(fld reflect.StructField) string {
 | 
			
		||||
		if fld.Tag.Get("validation_name") != "" {
 | 
			
		||||
			return fld.Tag.Get("validation_name")
 | 
			
		||||
		} else {
 | 
			
		||||
			return fld.Tag.Get("json")
 | 
			
		||||
		}
 | 
			
		||||
	})
 | 
			
		||||
	validation.RegisterValidation(
 | 
			
		||||
		"http_method",
 | 
			
		||||
		func(fl validator.FieldLevel) bool {
 | 
			
		||||
			method := fl.Field().String()
 | 
			
		||||
			// net/http/request.go/validMethod
 | 
			
		||||
			return len(method) > 0 && strings.IndexFunc(method, isNotToken) == -1
 | 
			
		||||
		},
 | 
			
		||||
	)
 | 
			
		||||
	validation.RegisterValidation(
 | 
			
		||||
		"string_bool",
 | 
			
		||||
		func(fl validator.FieldLevel) bool {
 | 
			
		||||
			s := fl.Field().String()
 | 
			
		||||
			return s == "true" || s == "false" || s == ""
 | 
			
		||||
		},
 | 
			
		||||
	)
 | 
			
		||||
	validation.RegisterValidation(
 | 
			
		||||
		"proxy_url",
 | 
			
		||||
		func(fl validator.FieldLevel) bool {
 | 
			
		||||
			url := fl.Field().String()
 | 
			
		||||
			if url == "" {
 | 
			
		||||
				return false
 | 
			
		||||
			}
 | 
			
		||||
			if err := validation.Var(url, "url"); err != nil {
 | 
			
		||||
				return false
 | 
			
		||||
			}
 | 
			
		||||
			if !(url[:7] == "http://" ||
 | 
			
		||||
				url[:9] == "socks5://" ||
 | 
			
		||||
				url[:10] == "socks5h://") {
 | 
			
		||||
				return false
 | 
			
		||||
			}
 | 
			
		||||
			return true
 | 
			
		||||
		},
 | 
			
		||||
	)
 | 
			
		||||
	return validation
 | 
			
		||||
}
 | 
			
		||||
		Reference in New Issue
	
	Block a user