mirror of
				https://github.com/aykhans/dodo.git
				synced 2025-10-31 04:29:59 +00:00 
			
		
		
		
	Compare commits
	
		
			1 Commits
		
	
	
		
			dependabot
			...
			39d366aadb
		
	
	| Author | SHA1 | Date | |
|---|---|---|---|
| 39d366aadb | 
| @@ -4,7 +4,6 @@ binaries | ||||
| dodo | ||||
| .git | ||||
| .gitignore | ||||
| .golangci.yml | ||||
| README.md | ||||
| LICENSE | ||||
| config.json | ||||
|   | ||||
							
								
								
									
										25
									
								
								.github/workflows/golangci-lint.yml
									
									
									
									
										vendored
									
									
								
							
							
						
						
									
										25
									
								
								.github/workflows/golangci-lint.yml
									
									
									
									
										vendored
									
									
								
							| @@ -1,25 +0,0 @@ | ||||
| name: golangci-lint | ||||
|  | ||||
| on: | ||||
|   push: | ||||
|     branches: | ||||
|       - main | ||||
|   pull_request: | ||||
|  | ||||
| permissions: | ||||
|   contents: read | ||||
|  | ||||
| jobs: | ||||
|   golangci: | ||||
|     name: lint | ||||
|     runs-on: ubuntu-latest | ||||
|     steps: | ||||
|       - uses: actions/checkout@v4 | ||||
|       - uses: actions/setup-go@v5 | ||||
|         with: | ||||
|           go-version: stable | ||||
|       - name: golangci-lint | ||||
|         uses: golangci/golangci-lint-action@v7 | ||||
|         with: | ||||
|           version: v2.4.0 | ||||
|           args: --timeout=10m --config=.golangci.yml | ||||
| @@ -1,33 +0,0 @@ | ||||
| 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,17 +1,19 @@ | ||||
| FROM golang:1.25-alpine AS builder | ||||
| FROM golang:1.23.2-alpine AS builder | ||||
|  | ||||
| WORKDIR /src | ||||
| WORKDIR /dodo | ||||
|  | ||||
| COPY go.mod go.sum ./ | ||||
| RUN go mod download | ||||
| COPY . . | ||||
|  | ||||
| RUN CGO_ENABLED=0 go build -ldflags "-s -w" -o dodo | ||||
| RUN go build -ldflags "-s -w" -o dodo | ||||
| RUN echo "{}" > config.json | ||||
|  | ||||
| FROM gcr.io/distroless/static-debian12:latest | ||||
|  | ||||
| WORKDIR / | ||||
| WORKDIR /dodo | ||||
|  | ||||
| COPY --from=builder /src/dodo /dodo | ||||
| COPY --from=builder /dodo/dodo /dodo/dodo | ||||
| COPY --from=builder /dodo/config.json /dodo/config.json | ||||
|  | ||||
| ENTRYPOINT ["./dodo"] | ||||
| ENTRYPOINT ["./dodo", "-c", "/dodo/config.json"] | ||||
							
								
								
									
										934
									
								
								EXAMPLES.md
									
									
									
									
									
								
							
							
						
						
									
										934
									
								
								EXAMPLES.md
									
									
									
									
									
								
							| @@ -1,934 +0,0 @@ | ||||
| # 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`. | ||||
							
								
								
									
										459
									
								
								README.md
									
									
									
									
									
								
							
							
						
						
									
										459
									
								
								README.md
									
									
									
									
									
								
							| @@ -1,340 +1,119 @@ | ||||
| <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 | ||||
|  | ||||
| ### Using Docker (Recommended) | ||||
|  | ||||
| Pull the latest Dodo image from Docker Hub: | ||||
|  | ||||
| ```sh | ||||
| docker pull aykhans/dodo:latest | ||||
| ``` | ||||
|  | ||||
| To use Dodo with Docker and a local config file, mount the config file as a volume and pass it as an argument: | ||||
|  | ||||
| ```sh | ||||
| docker run -v /path/to/config.json:/config.json aykhans/dodo -f /config.json | ||||
| ``` | ||||
|  | ||||
| If you're using a remote config file via URL, you don't need to mount a volume: | ||||
|  | ||||
| ```sh | ||||
| docker run aykhans/dodo -f https://raw.githubusercontent.com/aykhans/dodo/main/config.yaml | ||||
| ``` | ||||
|  | ||||
| ### Using Pre-built Binaries | ||||
|  | ||||
| Download the latest binaries from the [releases](https://github.com/aykhans/dodo/releases) section. | ||||
|  | ||||
| ### Building from Source | ||||
|  | ||||
| To build Dodo from source, ensure you have [Go 1.24+](https://golang.org/dl/) installed. | ||||
|  | ||||
| ```sh | ||||
| go install -ldflags "-s -w" github.com/aykhans/dodo@latest | ||||
| ``` | ||||
|  | ||||
| ## Usage | ||||
|  | ||||
| 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 -o 1m -t 2s | ||||
| ``` | ||||
|  | ||||
| With Docker: | ||||
|  | ||||
| ```sh | ||||
| docker run --rm -i aykhans/dodo -u https://example.com -m GET -d 10 -r 1000 -o 1m -t 2s | ||||
| ``` | ||||
|  | ||||
| ### 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", | ||||
|     "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.json | ||||
| # OR | ||||
| dodo -f https://example.com/config.json | ||||
| ``` | ||||
|  | ||||
| With Docker: | ||||
|  | ||||
| ```sh | ||||
| docker run --rm -i -v /path/to/config.json:/config.json aykhans/dodo | ||||
| # OR | ||||
| docker run --rm -i aykhans/dodo -f https://example.com/config.json | ||||
| ``` | ||||
|  | ||||
| ### 3. 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/to/config.json:/config.json aykhans/dodo -f /config.json -u https://example.com -m GET -d 10 -r 1000 -o 1m -t 5s | ||||
| ``` | ||||
|  | ||||
| You can find more usage examples in the [EXAMPLES.md](./EXAMPLES.md) file. | ||||
|  | ||||
| ## Config Parameters Reference | ||||
|  | ||||
| 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`. | ||||
| <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://raw.githubusercontent.com/aykhans/dodo/main/assets/dodo.png"> | ||||
| </p> | ||||
|  | ||||
| ## Installation | ||||
| ### With Docker (Recommended) | ||||
| Pull the 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. | ||||
|  | ||||
| ### Build from Source | ||||
| To build Dodo from source, you need to have [Go1.22+](https://golang.org/dl/) installed. <br> | ||||
| Follow the steps below to build dodo: | ||||
|  | ||||
| 1. **Clone the repository:** | ||||
|  | ||||
|     ```sh | ||||
|     git clone https://github.com/aykhans/dodo.git | ||||
|     ``` | ||||
|  | ||||
| 2. **Navigate to the project directory:** | ||||
|  | ||||
|     ```sh | ||||
|     cd dodo | ||||
|     ``` | ||||
|  | ||||
| 3. **Build the project:** | ||||
|  | ||||
|     ```sh | ||||
|     go build -ldflags "-s -w" -o dodo | ||||
|     ``` | ||||
|  | ||||
| This will generate an executable named `dodo` in the project directory. | ||||
|  | ||||
| ## Usage | ||||
| 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: | ||||
|  | ||||
| ```sh | ||||
| dodo -u https://example.com -m GET -d 10 -r 1000 -t 2000 | ||||
| ``` | ||||
| With Docker: | ||||
| ```sh | ||||
| docker run --rm aykhans/dodo -u https://example.com -m GET -d 10 -r 1000 -t 2000 | ||||
| ``` | ||||
|  | ||||
| ### 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 | ||||
| { | ||||
|     "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" | ||||
|         } | ||||
|     ] | ||||
| } | ||||
| ``` | ||||
| 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 | ||||
| ``` | ||||
| With Docker: | ||||
| ```sh | ||||
| docker run --rm -v ./path/config.json:/dodo/config.json -i aykhans/dodo | ||||
| ``` | ||||
|  | ||||
| ### 3. Both (CLI & JSON) | ||||
| Override the config file arguments with CLI arguments: | ||||
|  | ||||
| ```sh | ||||
| dodo -c /path/config.json -u https://example.com -m GET -d 10 -r 1000 -t 2000 | ||||
| ``` | ||||
| With Docker: | ||||
| ```sh | ||||
| docker run --rm -v ./path/config.json:/dodo/config.json -i aykhans/dodo -u https://example.com -m GET -d 10 -r 1000 -t 2000 | ||||
| ``` | ||||
|  | ||||
| ## 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. | ||||
|  | ||||
| | 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) | -           | | ||||
|   | ||||
| @@ -1,53 +0,0 @@ | ||||
| # https://taskfile.dev | ||||
|  | ||||
| version: "3" | ||||
|  | ||||
| vars: | ||||
|     PLATFORMS: | ||||
|         - os: darwin | ||||
|           archs: [amd64, arm64] | ||||
|         - os: freebsd | ||||
|           archs: [386, amd64, arm] | ||||
|         - os: linux | ||||
|           archs: [386, amd64, arm, arm64] | ||||
|         - os: netbsd | ||||
|           archs: [386, amd64, arm] | ||||
|         - os: openbsd | ||||
|           archs: [386, amd64, arm, arm64] | ||||
|         - os: windows | ||||
|           archs: [386, amd64, arm64] | ||||
|  | ||||
| tasks: | ||||
|     run: go run main.go | ||||
|  | ||||
|     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" | ||||
							
								
								
									
										
											BIN
										
									
								
								assets/dodo.png
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										
											BIN
										
									
								
								assets/dodo.png
									
									
									
									
									
										Normal file
									
								
							
										
											Binary file not shown.
										
									
								
							| After Width: | Height: | Size: 230 KiB | 
							
								
								
									
										32
									
								
								build.sh
									
									
									
									
									
										Executable file
									
								
							
							
						
						
									
										32
									
								
								build.sh
									
									
									
									
									
										Executable file
									
								
							| @@ -0,0 +1,32 @@ | ||||
| #!/bin/bash | ||||
|  | ||||
| platforms=( | ||||
|     "darwin,amd64" | ||||
|     "darwin,arm64" | ||||
|     "freebsd,386" | ||||
|     "freebsd,amd64" | ||||
|     "freebsd,arm" | ||||
|     "linux,386" | ||||
|     "linux,amd64" | ||||
|     "linux,arm" | ||||
|     "linux,arm64" | ||||
|     "netbsd,386" | ||||
|     "netbsd,amd64" | ||||
|     "netbsd,arm" | ||||
|     "openbsd,386" | ||||
|     "openbsd,amd64" | ||||
|     "openbsd,arm" | ||||
|     "openbsd,arm64" | ||||
|     "windows,386" | ||||
|     "windows,amd64" | ||||
|     "windows,arm64" | ||||
| ) | ||||
|  | ||||
| for platform in "${platforms[@]}"; do | ||||
|     IFS=',' read -r build_os build_arch <<< "$platform" | ||||
|     ext="" | ||||
|     if [ "$build_os" == "windows" ]; then | ||||
|         ext=".exe" | ||||
|     fi | ||||
|     GOOS="$build_os" GOARCH="$build_arch" go build -ldflags "-s -w" -o "./binaries/dodo-$build_os-$build_arch$ext" | ||||
| done | ||||
							
								
								
									
										51
									
								
								config.json
									
									
									
									
									
								
							
							
						
						
									
										51
									
								
								config.json
									
									
									
									
									
								
							| @@ -1,37 +1,22 @@ | ||||
| { | ||||
|     "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-text1", "body-text2", "body-text3"], | ||||
|  | ||||
|     "proxy": [ | ||||
|         "http://example.com:8080", | ||||
|         "http://username:password@example.com:8080", | ||||
|         "socks5://example.com:8080", | ||||
|         "socks5h://example.com:8080" | ||||
|     "no_proxy_check": false, | ||||
|     "timeout": 10000, | ||||
|     "dodos_count": 50, | ||||
|     "request_count": 1000, | ||||
|     "params": {}, | ||||
|     "headers": {}, | ||||
|     "cookies": {}, | ||||
|     "body": [""], | ||||
|     "proxies": [ | ||||
|         { | ||||
|             "url": "http://example.com:8080", | ||||
|             "username": "username", | ||||
|             "password": "password" | ||||
|         }, | ||||
|         { | ||||
|             "url": "http://example.com:8080" | ||||
|         } | ||||
|     ] | ||||
| } | ||||
| } | ||||
							
								
								
									
										40
									
								
								config.yaml
									
									
									
									
									
								
							
							
						
						
									
										40
									
								
								config.yaml
									
									
									
									
									
								
							| @@ -1,40 +0,0 @@ | ||||
| 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
									
									
									
									
									
								
							
							
						
						
									
										188
									
								
								config/cli.go
									
									
									
									
									
								
							| @@ -1,188 +0,0 @@ | ||||
| package config | ||||
|  | ||||
| import ( | ||||
| 	"flag" | ||||
| 	"fmt" | ||||
| 	"os" | ||||
| 	"strings" | ||||
| 	"time" | ||||
|  | ||||
| 	"github.com/aykhans/dodo/types" | ||||
| 	"github.com/aykhans/dodo/utils" | ||||
| ) | ||||
|  | ||||
| const cliUsageText = `Usage: | ||||
|   dodo [flags] | ||||
|  | ||||
| Examples: | ||||
|  | ||||
| Simple usage: | ||||
|   dodo -u https://example.com -o 1m | ||||
|  | ||||
| Usage with config file: | ||||
|   dodo -f /path/to/config/file/config.json | ||||
|  | ||||
| Usage with all flags: | ||||
|   dodo -f /path/to/config/file/config.json \ | ||||
|     -u https://example.com -m POST \ | ||||
|     -d 10 -r 1000 -o 3m -t 3s \ | ||||
|     -b "body1" -body "body2" \ | ||||
|     -H "header1:value1" -header "header2:value2" \ | ||||
|     -p "param1=value1" -param "param2=value2" \ | ||||
|     -c "cookie1=value1" -cookie "cookie2=value2" \ | ||||
|     -x "http://proxy.example.com:8080" -proxy "socks5://proxy2.example.com:8080" \ | ||||
|     -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" | ||||
| } | ||||
							
								
								
									
										391
									
								
								config/config.go
									
									
									
									
									
								
							
							
						
						
									
										391
									
								
								config/config.go
									
									
									
									
									
								
							| @@ -1,326 +1,159 @@ | ||||
| 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/utils" | ||||
| 	"github.com/jedib0t/go-pretty/v6/table" | ||||
| ) | ||||
|  | ||||
| const ( | ||||
| 	VERSION             string        = "0.7.3" | ||||
| 	DefaultUserAgent    string        = "Dodo/" + VERSION | ||||
| 	DefaultMethod       string        = "GET" | ||||
| 	DefaultTimeout      time.Duration = time.Second * 10 | ||||
| 	DefaultDodosCount   uint          = 1 | ||||
| 	DefaultRequestCount uint          = 0 | ||||
| 	DefaultDuration     time.Duration = 0 | ||||
| 	DefaultYes          bool          = false | ||||
| 	DefaultSkipVerify   bool          = false | ||||
| 	VERSION                 string = "0.5.2" | ||||
| 	DefaultUserAgent        string = "Dodo/" + VERSION | ||||
| 	ProxyCheckURL           string = "https://www.google.com" | ||||
| 	DefaultMethod           string = "GET" | ||||
| 	DefaultTimeout          uint32 = 10000 // Milliseconds (10 seconds) | ||||
| 	DefaultDodosCount       uint   = 1 | ||||
| 	DefaultRequestCount     uint   = 1000 | ||||
| 	MaxDodosCountForProxies uint   = 20 // Max dodos count for proxy check | ||||
| ) | ||||
|  | ||||
| var SupportedProxySchemes []string = []string{"http", "socks5", "socks5h"} | ||||
| type IConfig interface { | ||||
| 	MergeConfigs(newConfig IConfig) IConfig | ||||
| } | ||||
|  | ||||
| type RequestConfig struct { | ||||
| 	Method       string | ||||
| 	URL          url.URL | ||||
| 	URL          *url.URL | ||||
| 	Timeout      time.Duration | ||||
| 	DodosCount   uint | ||||
| 	RequestCount uint | ||||
| 	Duration     time.Duration | ||||
| 	Params       map[string][]string | ||||
| 	Headers      map[string][]string | ||||
| 	Cookies      map[string][]string | ||||
| 	Proxies      []Proxy | ||||
| 	Body         []string | ||||
| 	Yes          bool | ||||
| 	SkipVerify   bool | ||||
| 	Params       types.Params | ||||
| 	Headers      types.Headers | ||||
| 	Cookies      types.Cookies | ||||
| 	Body         types.Body | ||||
| 	Proxies      types.Proxies | ||||
| 	NoProxyCheck bool | ||||
| } | ||||
|  | ||||
| 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() { | ||||
| func (config *RequestConfig) Print() { | ||||
| 	t := table.NewWriter() | ||||
| 	t.SetOutputMirror(os.Stdout) | ||||
| 	t.SetStyle(table.StyleLight) | ||||
| 	t.SetColumnConfigs([]table.ColumnConfig{ | ||||
| 		{ | ||||
| 			Number: 2, | ||||
| 			WidthMaxEnforcer: func(col string, maxLen int) string { | ||||
| 				lines := strings.Split(col, "\n") | ||||
| 				for i, line := range lines { | ||||
| 					if len(line) > maxLen { | ||||
| 						lines[i] = line[:maxLen-3] + "..." | ||||
| 					} | ||||
| 				} | ||||
| 				return strings.Join(lines, "\n") | ||||
| 			}, | ||||
| 			WidthMax: 50}, | ||||
| 		{Number: 2, WidthMax: 50}, | ||||
| 	}) | ||||
|  | ||||
| 	t.AppendHeader(table.Row{"Request Configuration"}) | ||||
| 	t.AppendRow(table.Row{"URL", rc.URL.String()}) | ||||
| 	t.AppendRow(table.Row{"Method", config.Method}) | ||||
| 	t.AppendSeparator() | ||||
| 	t.AppendRow(table.Row{"Method", rc.Method}) | ||||
| 	t.AppendRow(table.Row{"URL", config.URL}) | ||||
| 	t.AppendSeparator() | ||||
| 	t.AppendRow(table.Row{"Timeout", rc.Timeout}) | ||||
| 	t.AppendRow(table.Row{"Timeout", fmt.Sprintf("%dms", config.Timeout/time.Millisecond)}) | ||||
| 	t.AppendSeparator() | ||||
| 	t.AppendRow(table.Row{"Dodos", rc.DodosCount}) | ||||
| 	t.AppendRow(table.Row{"Dodos", config.DodosCount}) | ||||
| 	t.AppendSeparator() | ||||
| 	if rc.RequestCount > 0 { | ||||
| 		t.AppendRow(table.Row{"Requests", rc.RequestCount}) | ||||
| 	} else { | ||||
| 		t.AppendRow(table.Row{"Requests"}) | ||||
| 	} | ||||
| 	t.AppendRow(table.Row{"Request", config.RequestCount}) | ||||
| 	t.AppendSeparator() | ||||
| 	if rc.Duration > 0 { | ||||
| 		t.AppendRow(table.Row{"Duration", rc.Duration}) | ||||
| 	} else { | ||||
| 		t.AppendRow(table.Row{"Duration"}) | ||||
| 	} | ||||
| 	t.AppendRow(table.Row{"Params", utils.MarshalJSON(config.Params, 3)}) | ||||
| 	t.AppendSeparator() | ||||
| 	t.AppendRow(table.Row{"Params", rc.Params.String()}) | ||||
| 	t.AppendRow(table.Row{"Headers", utils.MarshalJSON(config.Headers, 3)}) | ||||
| 	t.AppendSeparator() | ||||
| 	t.AppendRow(table.Row{"Headers", rc.Headers.String()}) | ||||
| 	t.AppendRow(table.Row{"Cookies", utils.MarshalJSON(config.Cookies, 3)}) | ||||
| 	t.AppendSeparator() | ||||
| 	t.AppendRow(table.Row{"Cookies", rc.Cookies.String()}) | ||||
| 	t.AppendRow(table.Row{"Proxies Count", len(config.Proxies)}) | ||||
| 	t.AppendSeparator() | ||||
| 	t.AppendRow(table.Row{"Proxy", rc.Proxies.String()}) | ||||
| 	t.AppendRow(table.Row{"Proxy Check", !config.NoProxyCheck}) | ||||
| 	t.AppendSeparator() | ||||
| 	t.AppendRow(table.Row{"Body", rc.Body.String()}) | ||||
| 	t.AppendSeparator() | ||||
| 	t.AppendRow(table.Row{"Skip Verify", rc.SkipVerify}) | ||||
| 	t.AppendRow(table.Row{"Body", utils.MarshalJSON(config.Body, 3)}) | ||||
|  | ||||
| 	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" 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"` | ||||
| 	Method       string             `json:"method" validate:"http_method"` // custom validations: http_method | ||||
| 	URL          string             `json:"url" validate:"http_url,required"` | ||||
| 	Timeout      uint32             `json:"timeout" validate:"gte=1,lte=100000"` | ||||
| 	DodosCount   uint               `json:"dodos_count" validate:"gte=1"` | ||||
| 	RequestCount uint               `json:"request_count" validation_name:"request-count" validate:"gte=1"` | ||||
| 	NoProxyCheck utils.Option[bool] `json:"no_proxy_check"` | ||||
| } | ||||
|  | ||||
| func NewConfig() *Config { | ||||
| 	return &Config{} | ||||
| } | ||||
|  | ||||
| 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 { | ||||
| func (config *Config) MergeConfigs(newConfig *Config) { | ||||
| 	if newConfig.Method != "" { | ||||
| 		config.Method = newConfig.Method | ||||
| 	} | ||||
| 	if newConfig.URL != nil { | ||||
| 	if newConfig.URL != "" { | ||||
| 		config.URL = newConfig.URL | ||||
| 	} | ||||
| 	if newConfig.Timeout != nil { | ||||
| 	if newConfig.Timeout != 0 { | ||||
| 		config.Timeout = newConfig.Timeout | ||||
| 	} | ||||
| 	if newConfig.DodosCount != nil { | ||||
| 	if newConfig.DodosCount != 0 { | ||||
| 		config.DodosCount = newConfig.DodosCount | ||||
| 	} | ||||
| 	if newConfig.RequestCount != nil { | ||||
| 	if newConfig.RequestCount != 0 { | ||||
| 		config.RequestCount = newConfig.RequestCount | ||||
| 	} | ||||
| 	if newConfig.Duration != nil { | ||||
| 		config.Duration = newConfig.Duration | ||||
| 	if !newConfig.NoProxyCheck.IsNone() { | ||||
| 		config.NoProxyCheck = newConfig.NoProxyCheck | ||||
| 	} | ||||
| 	if newConfig.Yes != nil { | ||||
| 		config.Yes = newConfig.Yes | ||||
| } | ||||
|  | ||||
| func (config *Config) SetDefaults() { | ||||
| 	if config.Method == "" { | ||||
| 		config.Method = DefaultMethod | ||||
| 	} | ||||
| 	if newConfig.SkipVerify != nil { | ||||
| 		config.SkipVerify = newConfig.SkipVerify | ||||
| 	if config.Timeout == 0 { | ||||
| 		config.Timeout = DefaultTimeout | ||||
| 	} | ||||
| 	if config.DodosCount == 0 { | ||||
| 		config.DodosCount = DefaultDodosCount | ||||
| 	} | ||||
| 	if config.RequestCount == 0 { | ||||
| 		config.RequestCount = DefaultRequestCount | ||||
| 	} | ||||
| 	if config.NoProxyCheck.IsNone() { | ||||
| 		config.NoProxyCheck = utils.NewOption(false) | ||||
| 	} | ||||
| } | ||||
|  | ||||
| type Proxy struct { | ||||
| 	URL      string `json:"url" validate:"required,proxy_url"` | ||||
| 	Username string `json:"username"` | ||||
| 	Password string `json:"password"` | ||||
| } | ||||
|  | ||||
| type JSONConfig struct { | ||||
| 	Config | ||||
| 	Params  map[string][]string `json:"params"` | ||||
| 	Headers map[string][]string `json:"headers"` | ||||
| 	Cookies map[string][]string `json:"cookies"` | ||||
| 	Proxies []Proxy             `json:"proxies" validate:"dive"` | ||||
| 	Body    []string            `json:"body"` | ||||
| } | ||||
|  | ||||
| func (config *JSONConfig) MergeConfigs(newConfig *JSONConfig) { | ||||
| 	config.Config.MergeConfigs(&newConfig.Config) | ||||
| 	if len(newConfig.Params) != 0 { | ||||
| 		config.Params = newConfig.Params | ||||
| 	} | ||||
| @@ -338,27 +171,15 @@ func (config *Config) MergeConfig(newConfig *Config) { | ||||
| 	} | ||||
| } | ||||
|  | ||||
| func (config *Config) SetDefaults() { | ||||
| 	if config.Method == nil { | ||||
| 		config.Method = utils.ToPtr(DefaultMethod) | ||||
| 	} | ||||
| 	if config.Timeout == nil { | ||||
| 		config.Timeout = &types.Timeout{Duration: DefaultTimeout} | ||||
| 	} | ||||
| 	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) | ||||
| type CLIConfig struct { | ||||
| 	Config | ||||
| 	Yes        bool   `json:"yes" validate:"omitempty"` | ||||
| 	ConfigFile string `validation_name:"config-file" validate:"omitempty,filepath"` | ||||
| } | ||||
|  | ||||
| func (config *CLIConfig) MergeConfigs(newConfig *CLIConfig) { | ||||
| 	config.Config.MergeConfigs(&newConfig.Config) | ||||
| 	if newConfig.ConfigFile != "" { | ||||
| 		config.ConfigFile = newConfig.ConfigFile | ||||
| 	} | ||||
| } | ||||
|   | ||||
| @@ -1,84 +0,0 @@ | ||||
| package config | ||||
|  | ||||
| import ( | ||||
| 	"encoding/json" | ||||
| 	"errors" | ||||
| 	"fmt" | ||||
| 	"io" | ||||
| 	"net/http" | ||||
| 	"os" | ||||
| 	"slices" | ||||
| 	"strings" | ||||
| 	"time" | ||||
|  | ||||
| 	"github.com/aykhans/dodo/types" | ||||
| 	"gopkg.in/yaml.v3" | ||||
| ) | ||||
|  | ||||
| var supportedFileTypes = []string{"json", "yaml", "yml"} | ||||
|  | ||||
| func (config *Config) ReadFile(filePath types.ConfigFile) error { | ||||
| 	var ( | ||||
| 		data []byte | ||||
| 		err  error | ||||
| 	) | ||||
|  | ||||
| 	fileExt := filePath.Extension() | ||||
| 	if slices.Contains(supportedFileTypes, fileExt) { | ||||
| 		if filePath.LocationType() == types.FileLocationTypeRemoteHTTP { | ||||
| 			client := &http.Client{ | ||||
| 				Timeout: 10 * time.Second, | ||||
| 			} | ||||
|  | ||||
| 			resp, err := client.Get(filePath.String()) | ||||
| 			if err != nil { | ||||
| 				return fmt.Errorf("failed to fetch config file from %s", filePath) | ||||
| 			} | ||||
| 			defer func() { _ = resp.Body.Close() }() | ||||
|  | ||||
| 			data, err = io.ReadAll(io.Reader(resp.Body)) | ||||
| 			if err != nil { | ||||
| 				return fmt.Errorf("failed to read config file from %s", filePath) | ||||
| 			} | ||||
| 		} else { | ||||
| 			data, err = os.ReadFile(filePath.String()) | ||||
| 			if err != nil { | ||||
| 				return errors.New("failed to read config file from " + filePath.String()) | ||||
| 			} | ||||
| 		} | ||||
|  | ||||
| 		switch fileExt { | ||||
| 		case "json": | ||||
| 			return parseJSONConfig(data, config) | ||||
| 		case "yml", "yaml": | ||||
| 			return parseYAMLConfig(data, config) | ||||
| 		} | ||||
| 	} | ||||
|  | ||||
| 	return fmt.Errorf("unsupported config file type (supported types: %v)", strings.Join(supportedFileTypes, ", ")) | ||||
| } | ||||
|  | ||||
| func parseJSONConfig(data []byte, config *Config) error { | ||||
| 	err := json.Unmarshal(data, &config) | ||||
| 	if err != nil { | ||||
| 		switch parsedErr := err.(type) { | ||||
| 		case *json.SyntaxError: | ||||
| 			return fmt.Errorf("JSON Config file: invalid syntax at byte offset %d", parsedErr.Offset) | ||||
| 		case *json.UnmarshalTypeError: | ||||
| 			return fmt.Errorf("JSON Config file: invalid type %v for field %s, expected %v", parsedErr.Value, parsedErr.Field, parsedErr.Type) | ||||
| 		default: | ||||
| 			return fmt.Errorf("JSON Config file: %s", err.Error()) | ||||
| 		} | ||||
| 	} | ||||
|  | ||||
| 	return nil | ||||
| } | ||||
|  | ||||
| func parseYAMLConfig(data []byte, config *Config) error { | ||||
| 	err := yaml.Unmarshal(data, &config) | ||||
| 	if err != nil { | ||||
| 		return fmt.Errorf("YAML Config file: %s", err.Error()) | ||||
| 	} | ||||
|  | ||||
| 	return nil | ||||
| } | ||||
							
								
								
									
										117
									
								
								custom_errors/errors.go
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										117
									
								
								custom_errors/errors.go
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,117 @@ | ||||
| package customerrors | ||||
|  | ||||
| import ( | ||||
| 	"errors" | ||||
| 	"fmt" | ||||
|  | ||||
| 	"github.com/go-playground/validator/v10" | ||||
| ) | ||||
|  | ||||
| var ( | ||||
| 	ErrInvalidJSON = errors.New("invalid JSON file") | ||||
| 	ErrInvalidFile = errors.New("invalid file") | ||||
| 	ErrInterrupt   = errors.New("interrupted") | ||||
| 	ErrNoInternet  = errors.New("no internet connection") | ||||
| 	ErrTimeout     = errors.New("timeout") | ||||
| ) | ||||
|  | ||||
| func As(err error, target any) bool { | ||||
| 	return errors.As(err, target) | ||||
| } | ||||
|  | ||||
| func Is(err, target error) bool { | ||||
| 	return errors.Is(err, target) | ||||
| } | ||||
|  | ||||
| type Error interface { | ||||
| 	Error() string | ||||
| 	Unwrap() error | ||||
| } | ||||
|  | ||||
| type TypeError struct { | ||||
| 	Expected string | ||||
| 	Received string | ||||
| 	Field    string | ||||
| 	err      error | ||||
| } | ||||
|  | ||||
| func NewTypeError(expected, received, field string, err error) *TypeError { | ||||
| 	return &TypeError{ | ||||
| 		Expected: expected, | ||||
| 		Received: received, | ||||
| 		Field:    field, | ||||
| 		err:      err, | ||||
| 	} | ||||
| } | ||||
|  | ||||
| func (e *TypeError) Error() string { | ||||
| 	return "Expected " + e.Expected + " but received " + e.Received + " in field " + e.Field | ||||
| } | ||||
|  | ||||
| func (e *TypeError) Unwrap() error { | ||||
| 	return e.err | ||||
| } | ||||
|  | ||||
| type InvalidFileError struct { | ||||
| 	FileName string | ||||
| 	err      error | ||||
| } | ||||
|  | ||||
| func NewInvalidFileError(fileName string, err error) *InvalidFileError { | ||||
| 	return &InvalidFileError{ | ||||
| 		FileName: fileName, | ||||
| 		err:      err, | ||||
| 	} | ||||
| } | ||||
|  | ||||
| func (e *InvalidFileError) Error() string { | ||||
| 	return "Invalid file: " + e.FileName | ||||
| } | ||||
|  | ||||
| func (e *InvalidFileError) Unwrap() error { | ||||
| 	return e.err | ||||
| } | ||||
|  | ||||
| type FileNotFoundError struct { | ||||
| 	FileName string | ||||
| 	err      error | ||||
| } | ||||
|  | ||||
| func NewFileNotFoundError(fileName string, err error) *FileNotFoundError { | ||||
| 	return &FileNotFoundError{ | ||||
| 		FileName: fileName, | ||||
| 		err:      err, | ||||
| 	} | ||||
| } | ||||
|  | ||||
| func (e *FileNotFoundError) Error() string { | ||||
| 	return "File not found: " + e.FileName | ||||
| } | ||||
|  | ||||
| func (e *FileNotFoundError) Unwrap() error { | ||||
| 	return e.err | ||||
| } | ||||
|  | ||||
| type ValidationErrors struct { | ||||
| 	MapErrors map[string]string | ||||
| 	errors    validator.ValidationErrors | ||||
| } | ||||
|  | ||||
| func NewValidationErrors(errsMap map[string]string, errs validator.ValidationErrors) *ValidationErrors { | ||||
| 	return &ValidationErrors{ | ||||
| 		MapErrors: errsMap, | ||||
| 		errors:    errs, | ||||
| 	} | ||||
| } | ||||
|  | ||||
| func (errs *ValidationErrors) Error() string { | ||||
| 	var errorsStr string | ||||
| 	for k, v := range errs.MapErrors { | ||||
| 		errorsStr += fmt.Sprintf("[%s]: %s\n", k, v) | ||||
| 	} | ||||
| 	return errorsStr | ||||
| } | ||||
|  | ||||
| func (errs *ValidationErrors) Unwrap() error { | ||||
| 	return errs.errors | ||||
| } | ||||
							
								
								
									
										72
									
								
								custom_errors/formaters.go
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										72
									
								
								custom_errors/formaters.go
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,72 @@ | ||||
| package customerrors | ||||
|  | ||||
| import ( | ||||
| 	"fmt" | ||||
| 	"net" | ||||
| 	"net/url" | ||||
| 	"strings" | ||||
|  | ||||
| 	"github.com/go-playground/validator/v10" | ||||
| ) | ||||
|  | ||||
| func OSErrorFormater(err error) error { | ||||
| 	errStr := err.Error() | ||||
| 	if strings.Contains(errStr, "no such file or directory") { | ||||
| 		fileName1 := strings.Index(errStr, "open") | ||||
| 		fileName2 := strings.LastIndex(errStr, ":") | ||||
| 		return NewFileNotFoundError(errStr[fileName1+5:fileName2], err) | ||||
| 	} | ||||
| 	return ErrInvalidFile | ||||
| } | ||||
|  | ||||
| func CobraErrorFormater(err error) error { | ||||
| 	return err | ||||
| } | ||||
|  | ||||
| func shortenNamespace(namespace string) string { | ||||
| 	return namespace[strings.Index(namespace, ".")+1:] | ||||
| } | ||||
|  | ||||
| func ValidationErrorsFormater(errs validator.ValidationErrors) error { | ||||
| 	errsStr := make(map[string]string) | ||||
| 	for _, err := range errs { | ||||
| 		switch err.Tag() { | ||||
| 		case "required": | ||||
| 			errsStr[shortenNamespace(err.Namespace())] = fmt.Sprintf("Field \"%s\" is required", err.Field()) | ||||
| 		case "gte": | ||||
| 			errsStr[shortenNamespace(err.Namespace())] = fmt.Sprintf("Value of \"%s\" must be greater than or equal to \"%s\"", err.Field(), err.Param()) | ||||
| 		case "lte": | ||||
| 			errsStr[shortenNamespace(err.Namespace())] = fmt.Sprintf("Value of \"%s\" must be less than or equal to \"%s\"", err.Field(), err.Param()) | ||||
| 		case "filepath": | ||||
| 			errsStr[shortenNamespace(err.Namespace())] = fmt.Sprintf("Invalid file path for \"%s\" field: \"%s\"", err.Field(), err.Value()) | ||||
| 		case "http_url": | ||||
| 			errsStr[shortenNamespace(err.Namespace())] = | ||||
| 				fmt.Sprintf("Invalid url for \"%s\" field: \"%s\"", err.Field(), err.Value()) | ||||
| 		// --------------------------------------| Custom validations |-------------------------------------- | ||||
| 		case "http_method": | ||||
| 			errsStr[shortenNamespace(err.Namespace())] = fmt.Sprintf("Invalid HTTP method for \"%s\" field: \"%s\"", err.Field(), err.Value()) | ||||
| 		case "proxy_url": | ||||
| 			errsStr[shortenNamespace(err.Namespace())] = fmt.Sprintf("Invalid proxy url for \"%s\" field: \"%s\" (it must be http, socks5 or socks5h)", err.Field(), err.Value()) | ||||
| 		case "string_bool": | ||||
| 			errsStr[shortenNamespace(err.Namespace())] = fmt.Sprintf("Invalid value for \"%s\" field: \"%s\"", err.Field(), err.Value()) | ||||
| 		default: | ||||
| 			errsStr[shortenNamespace(err.Namespace())] = fmt.Sprintf("Invalid value for \"%s\" field: \"%s\"", err.Field(), err.Value()) | ||||
| 		} | ||||
| 	} | ||||
| 	return NewValidationErrors(errsStr, errs) | ||||
| } | ||||
|  | ||||
| func RequestErrorsFormater(err error) string { | ||||
| 	switch e := err.(type) { | ||||
| 	case *url.Error: | ||||
| 		if netErr, ok := e.Err.(net.Error); ok && netErr.Timeout() { | ||||
| 			return "Timeout Error" | ||||
| 		} | ||||
| 		if strings.Contains(e.Error(), "http: ContentLength=") { | ||||
| 			println(e.Error()) | ||||
| 			return "Empty Body Error" | ||||
| 		} | ||||
| 		// TODO: Add more cases | ||||
| 	} | ||||
| 	return "Unknown Error" | ||||
| } | ||||
							
								
								
									
										31
									
								
								go.mod
									
									
									
									
									
								
							
							
						
						
									
										31
									
								
								go.mod
									
									
									
									
									
								
							| @@ -1,22 +1,29 @@ | ||||
| module github.com/aykhans/dodo | ||||
|  | ||||
| go 1.25 | ||||
| go 1.23.2 | ||||
|  | ||||
| require ( | ||||
| 	github.com/brianvoe/gofakeit/v7 v7.8.1 | ||||
| 	github.com/jedib0t/go-pretty/v6 v6.6.8 | ||||
| 	github.com/valyala/fasthttp v1.65.0 | ||||
| 	gopkg.in/yaml.v3 v3.0.1 | ||||
| 	github.com/go-playground/validator/v10 v10.23.0 | ||||
| 	github.com/jedib0t/go-pretty/v6 v6.6.2 | ||||
| 	github.com/spf13/cobra v1.8.1 | ||||
| 	github.com/spf13/pflag v1.0.5 | ||||
| 	github.com/valyala/fasthttp v1.57.0 | ||||
| 	golang.org/x/net v0.31.0 | ||||
| ) | ||||
|  | ||||
| require ( | ||||
| 	github.com/andybalholm/brotli v1.2.0 // indirect | ||||
| 	github.com/klauspost/compress v1.18.0 // indirect | ||||
| 	github.com/mattn/go-runewidth v0.0.16 // indirect | ||||
| 	github.com/andybalholm/brotli v1.1.1 // indirect | ||||
| 	github.com/gabriel-vasile/mimetype v1.4.4 // indirect | ||||
| 	github.com/go-playground/locales v0.14.1 // indirect | ||||
| 	github.com/go-playground/universal-translator v0.18.1 // indirect | ||||
| 	github.com/inconshreveable/mousetrap v1.1.0 // indirect | ||||
| 	github.com/klauspost/compress v1.17.11 // indirect | ||||
| 	github.com/leodido/go-urn v1.4.0 // indirect | ||||
| 	github.com/mattn/go-runewidth v0.0.15 // indirect | ||||
| 	github.com/rivo/uniseg v0.4.7 // indirect | ||||
| 	github.com/valyala/bytebufferpool v1.0.0 // indirect | ||||
| 	golang.org/x/net v0.43.0 // indirect | ||||
| 	golang.org/x/sys v0.35.0 // indirect | ||||
| 	golang.org/x/term v0.34.0 // indirect | ||||
| 	golang.org/x/text v0.28.0 // indirect | ||||
| 	golang.org/x/crypto v0.29.0 // indirect | ||||
| 	golang.org/x/sys v0.27.0 // indirect | ||||
| 	golang.org/x/term v0.26.0 // indirect | ||||
| 	golang.org/x/text v0.20.0 // indirect | ||||
| ) | ||||
|   | ||||
							
								
								
									
										65
									
								
								go.sum
									
									
									
									
									
								
							
							
						
						
									
										65
									
								
								go.sum
									
									
									
									
									
								
							| @@ -1,37 +1,56 @@ | ||||
| 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.8.1 h1:ZrN4tC2moLTOm6rjrE+dxlDA9bNH1v71LX8Nal1eyV4= | ||||
| github.com/brianvoe/gofakeit/v7 v7.8.1/go.mod h1:QXuPeBw164PJCzCUZVmgpgHJ3Llj49jSLVkKPMtxtxA= | ||||
| github.com/andybalholm/brotli v1.1.1 h1:PR2pgnyFznKEugtsUo0xLdDop5SKXd5Qf5ysW+7XdTA= | ||||
| github.com/andybalholm/brotli v1.1.1/go.mod h1:05ib4cKhjx3OQYUY22hTVd34Bc8upXjOLL2rKwwZBoA= | ||||
| github.com/cpuguy83/go-md2man/v2 v2.0.4/go.mod h1:tgQtvFlXSQOSOSIRvRPT7W67SCa46tRHOmNcaadrF8o= | ||||
| github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c= | ||||
| github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= | ||||
| 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.0 h1:c/Cqfb0r+Yi+JtIEq73FWXVkRonBlf0CRNYc8Zttxdo= | ||||
| github.com/klauspost/compress v1.18.0/go.mod h1:2Pp+KzxcywXVXMr50+X0Q/Lsb43OQHYWRCY2AiWywWQ= | ||||
| github.com/mattn/go-runewidth v0.0.16 h1:E5ScNMtiwvlvB5paMFdw9p4kSQzbXFikJ5SQO6TULQc= | ||||
| github.com/mattn/go-runewidth v0.0.16/go.mod h1:Jdepj2loyihRzMpdS35Xk/zdY8IAYHsh153qUoGf23w= | ||||
| 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/inconshreveable/mousetrap v1.1.0 h1:wN+x4NVGpMsO7ErUn/mUI3vEoE6Jt13X2s0bqwp9tc8= | ||||
| github.com/inconshreveable/mousetrap v1.1.0/go.mod h1:vpF70FUmC8bwa3OWnCshd2FqLfsEA9PFc4w1p2J65bw= | ||||
| github.com/jedib0t/go-pretty/v6 v6.6.2 h1:27bLj3nRODzaiA7tPIxy9UVWHoPspFfME9XxgwiiNsM= | ||||
| github.com/jedib0t/go-pretty/v6 v6.6.2/go.mod h1:zbn98qrYlh95FIhwwsbIip0LYpwSG8SUOScs+v9/t0E= | ||||
| github.com/klauspost/compress v1.17.11 h1:In6xLpyWOi1+C7tXUUWv2ot1QvBjxevKAaI6IXrJmUc= | ||||
| github.com/klauspost/compress v1.17.11/go.mod h1:pMDklpSncoRMuLFrf1W9Ss9KT+0rH90U12bZKk7uwG0= | ||||
| github.com/leodido/go-urn v1.4.0 h1:WT9HwE9SGECu3lg4d/dIA+jxlljEa1/ffXKmRjqdmIQ= | ||||
| github.com/leodido/go-urn v1.4.0/go.mod h1:bvxc+MVxLKB4z00jd1z+Dvzr47oO32F/QSNjSBOlFxI= | ||||
| github.com/mattn/go-runewidth v0.0.15 h1:UNAjwbU9l54TA3KzvqLGxwWjHmMgBUVhBiTjelZgg3U= | ||||
| github.com/mattn/go-runewidth v0.0.15/go.mod h1:Jdepj2loyihRzMpdS35Xk/zdY8IAYHsh153qUoGf23w= | ||||
| github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM= | ||||
| github.com/pmezard/go-difflib v1.0.0/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.10.0 h1:Xv5erBjTwe/5IxqUQTdXv5kgmIvbHo3QQyRwhJsOfJA= | ||||
| github.com/stretchr/testify v1.10.0/go.mod h1:r2ic/lqez/lEtzL7wO/rwa5dbSLXVDPFyf8C91i36aY= | ||||
| github.com/russross/blackfriday/v2 v2.1.0/go.mod h1:+Rmxgy9KzJVeS9/2gXHxylqXiyQDYRxCVz55jmeOWTM= | ||||
| github.com/spf13/cobra v1.8.1 h1:e5/vxKd/rZsfSJMUX1agtjeTDf+qv1/JdBF8gg5k9ZM= | ||||
| github.com/spf13/cobra v1.8.1/go.mod h1:wHxEcudfqmLYa8iTfL+OuZPbBZkmvliBWKIezN3kD9Y= | ||||
| github.com/spf13/pflag v1.0.5 h1:iy+VFUOCP1a+8yFto/drg2CJ5u0yRoB7fZw3DKv/JXA= | ||||
| github.com/spf13/pflag v1.0.5/go.mod h1:McXfInJRrz4CZXVZOBLb0bTZqETkiAhM9Iw0y3An2Bg= | ||||
| github.com/stretchr/testify v1.8.4 h1:CcVxjf3Q8PM0mHUKJCdn+eZZtm5yQwehR5yeSVQQcUk= | ||||
| github.com/stretchr/testify v1.8.4/go.mod h1:sz/lmYIOXD/1dqDmKjjqLyZ2RngseejIcXlSw2iwfAo= | ||||
| github.com/valyala/bytebufferpool v1.0.0 h1:GqA5TC/0021Y/b9FG4Oi9Mr3q7XYx6KllzawFIhcdPw= | ||||
| github.com/valyala/bytebufferpool v1.0.0/go.mod h1:6bBcMArwyJ5K/AmCkWv1jt77kVWyCJ6HpOuEn7z0Csc= | ||||
| github.com/valyala/fasthttp v1.65.0 h1:j/u3uzFEGFfRxw79iYzJN+TteTJwbYkru9uDp3d0Yf8= | ||||
| github.com/valyala/fasthttp v1.65.0/go.mod h1:P/93/YkKPMsKSnATEeELUCkG8a7Y+k99uxNHVbKINr4= | ||||
| github.com/valyala/fasthttp v1.57.0 h1:Xw8SjWGEP/+wAAgyy5XTvgrWlOD1+TxbbvNADYCm1Tg= | ||||
| github.com/valyala/fasthttp v1.57.0/go.mod h1:h6ZBaPRlzpZ6O3H5t2gEk1Qi33+TmLvfwgLLp0t9CpE= | ||||
| github.com/xyproto/randomstring v1.0.5 h1:YtlWPoRdgMu3NZtP45drfy1GKoojuR7hmRcnhZqKjWU= | ||||
| github.com/xyproto/randomstring v1.0.5/go.mod h1:rgmS5DeNXLivK7YprL0pY+lTuhNQW3iGxZ18UQApw/E= | ||||
| golang.org/x/net v0.43.0 h1:lat02VYK2j4aLzMzecihNvTlJNQUq316m2Mr9rnM6YE= | ||||
| golang.org/x/net v0.43.0/go.mod h1:vhO1fvI4dGsIjh73sWfUVjj3N7CA9WkKJNQm2svM6Jg= | ||||
| golang.org/x/sys v0.35.0 h1:vz1N37gP5bs89s7He8XuIYXpyY0+QlsKmzipCbUtyxI= | ||||
| golang.org/x/sys v0.35.0/go.mod h1:BJP2sWEmIv4KK5OTEluFJCKSidICx8ciO85XgH3Ak8k= | ||||
| golang.org/x/term v0.34.0 h1:O/2T7POpk0ZZ7MAzMeWFSg6S5IpWd/RXDlM9hgM3DR4= | ||||
| golang.org/x/term v0.34.0/go.mod h1:5jC53AEywhIVebHgPVeg0mj8OD3VO9OzclacVrqpaAw= | ||||
| golang.org/x/text v0.28.0 h1:rhazDwis8INMIwQ4tpjLDzUhx6RlXqZNPEM0huQojng= | ||||
| golang.org/x/text v0.28.0/go.mod h1:U8nCwOR8jO/marOQ0QbDiOngZVEBB7MAiitBuMjXiNU= | ||||
| gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405 h1:yhCVgyC4o1eVCa2tZl7eS0r+SDo693bJlVdllGtEeKM= | ||||
| golang.org/x/crypto v0.29.0 h1:L5SG1JTTXupVV3n6sUqMTeWbjAyfPwoda2DLX8J8FrQ= | ||||
| golang.org/x/crypto v0.29.0/go.mod h1:+F4F4N5hv6v38hfeYwTdx20oUvLLc+QfrE9Ax9HtgRg= | ||||
| golang.org/x/net v0.31.0 h1:68CPQngjLL0r2AlUKiSxtQFKvzRVbnzLwMUn5SzcLHo= | ||||
| golang.org/x/net v0.31.0/go.mod h1:P4fl1q7dY2hnZFxEk4pPSkDHF+QqjitcnDjUQyMM+pM= | ||||
| golang.org/x/sys v0.27.0 h1:wBqf8DvsY9Y/2P8gAfPDEYNuS30J4lPHJxXSb/nJZ+s= | ||||
| golang.org/x/sys v0.27.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA= | ||||
| golang.org/x/term v0.26.0 h1:WEQa6V3Gja/BhNxg540hBip/kkaYtRg3cxg4oXSw4AU= | ||||
| golang.org/x/term v0.26.0/go.mod h1:Si5m1o57C5nBNQo5z1iq+XDijt21BDBDp2bK0QI8e3E= | ||||
| golang.org/x/text v0.20.0 h1:gK/Kv2otX8gz+wn7Rmb3vT96ZwuoxnQlY+HlJVj7Qug= | ||||
| golang.org/x/text v0.20.0/go.mod h1:D4IsuqiFMhST5bX19pQ9ikHC2GsaKyk/oF+pn3ducp4= | ||||
| gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= | ||||
| gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA= | ||||
| gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= | ||||
|   | ||||
							
								
								
									
										117
									
								
								main.go
									
									
									
									
									
								
							
							
						
						
									
										117
									
								
								main.go
									
									
									
									
									
								
							| @@ -2,68 +2,115 @@ 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/jedib0t/go-pretty/v6/text" | ||||
| 	"github.com/aykhans/dodo/validation" | ||||
| 	goValidator "github.com/go-playground/validator/v10" | ||||
| ) | ||||
|  | ||||
| func main() { | ||||
| 	conf := config.NewConfig() | ||||
| 	configFile, err := conf.ReadCLI() | ||||
| 	validator := validation.NewValidator() | ||||
| 	conf := config.Config{} | ||||
| 	jsonConf := config.JSONConfig{} | ||||
|  | ||||
| 	cliConf, err := readers.CLIConfigReader() | ||||
| 	if err != nil || cliConf == nil { | ||||
| 		os.Exit(0) | ||||
| 	} | ||||
| 	if err := validator.StructPartial(cliConf, "ConfigFile"); err != nil { | ||||
| 		utils.PrintErrAndExit( | ||||
| 			customerrors.ValidationErrorsFormater( | ||||
| 				err.(goValidator.ValidationErrors), | ||||
| 			), | ||||
| 		) | ||||
| 	} | ||||
| 	if cliConf.ConfigFile != "" { | ||||
| 		jsonConfNew, err := readers.JSONConfigReader(cliConf.ConfigFile) | ||||
| 		if err != nil { | ||||
| 			utils.PrintErrAndExit(err) | ||||
| 		} | ||||
| 		if err := validator.StructFiltered( | ||||
| 			jsonConfNew, | ||||
| 			func(ns []byte) bool { | ||||
| 				return strings.LastIndex(string(ns), "Proxies") == -1 | ||||
| 			}); err != nil { | ||||
| 			utils.PrintErrAndExit( | ||||
| 				customerrors.ValidationErrorsFormater( | ||||
| 					err.(goValidator.ValidationErrors), | ||||
| 				), | ||||
| 			) | ||||
| 		} | ||||
| 		jsonConf = *jsonConfNew | ||||
| 		conf.MergeConfigs(&jsonConf.Config) | ||||
| 	} | ||||
|  | ||||
| 	conf.MergeConfigs(&cliConf.Config) | ||||
| 	conf.SetDefaults() | ||||
| 	if err := validator.Struct(conf); err != nil { | ||||
| 		utils.PrintErrAndExit( | ||||
| 			customerrors.ValidationErrorsFormater( | ||||
| 				err.(goValidator.ValidationErrors), | ||||
| 			), | ||||
| 		) | ||||
| 	} | ||||
|  | ||||
| 	parsedURL, err := url.Parse(conf.URL) | ||||
| 	if err != nil { | ||||
| 		utils.PrintErrAndExit(err) | ||||
| 	} | ||||
|  | ||||
| 	if configFile.String() != "" { | ||||
| 		tempConf := config.NewConfig() | ||||
| 		if err := tempConf.ReadFile(configFile); err != nil { | ||||
| 			utils.PrintErrAndExit(err) | ||||
| 		} | ||||
| 		tempConf.MergeConfig(conf) | ||||
| 		conf = tempConf | ||||
| 	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, | ||||
| 		NoProxyCheck: *conf.NoProxyCheck.ValueOrPanic(), | ||||
| 	} | ||||
| 	conf.SetDefaults() | ||||
|  | ||||
| 	if errs := conf.Validate(); len(errs) > 0 { | ||||
| 		utils.PrintErrAndExit(errors.Join(errs...)) | ||||
| 	} | ||||
|  | ||||
| 	requestConf := config.NewRequestConfig(conf) | ||||
| 	requestConf.Print() | ||||
|  | ||||
| 	if !requestConf.Yes { | ||||
| 		response := config.CLIYesOrNoReader("Do you want to continue?", false) | ||||
| 	if !cliConf.Yes { | ||||
| 		response := readers.CLIYesOrNoReader("Do you want to continue?", true) | ||||
| 		if !response { | ||||
| 			utils.PrintAndExit("Exiting...\n") | ||||
| 			utils.PrintAndExit("Exiting...") | ||||
| 		} | ||||
| 		fmt.Println() | ||||
| 	} | ||||
|  | ||||
| 	ctx, cancel := context.WithCancel(context.Background()) | ||||
| 	go listenForTermination(func() { cancel() }) | ||||
| 	sigChan := make(chan os.Signal, 1) | ||||
| 	signal.Notify(sigChan, syscall.SIGINT, syscall.SIGTERM) | ||||
| 	go func() { | ||||
| 		<-sigChan | ||||
| 		cancel() | ||||
| 	}() | ||||
|  | ||||
| 	responses, err := requests.Run(ctx, requestConf) | ||||
| 	if err != nil { | ||||
| 		if err == types.ErrInterrupt { | ||||
| 			fmt.Println(text.FgYellow.Sprint(err.Error())) | ||||
| 		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") | ||||
| 			return | ||||
| 		} | ||||
| 		utils.PrintErrAndExit(err) | ||||
| 		panic(err) | ||||
| 	} | ||||
|  | ||||
| 	responses.Print() | ||||
| } | ||||
|  | ||||
| func listenForTermination(do func()) { | ||||
| 	sigChan := make(chan os.Signal, 1) | ||||
| 	signal.Notify(sigChan, syscall.SIGINT, syscall.SIGTERM) | ||||
| 	<-sigChan | ||||
| 	do() | ||||
| } | ||||
|   | ||||
							
								
								
									
										100
									
								
								readers/cli.go
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										100
									
								
								readers/cli.go
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,100 @@ | ||||
| package readers | ||||
|  | ||||
| import ( | ||||
| 	"fmt" | ||||
|  | ||||
| 	"github.com/aykhans/dodo/config" | ||||
| 	"github.com/aykhans/dodo/custom_errors" | ||||
| 	"github.com/aykhans/dodo/utils" | ||||
| 	"github.com/spf13/cobra" | ||||
| 	"github.com/spf13/pflag" | ||||
| ) | ||||
|  | ||||
| func CLIConfigReader() (*config.CLIConfig, error) { | ||||
| 	var ( | ||||
| 		returnNil = false | ||||
| 		cliConfig = &config.CLIConfig{ | ||||
| 			Config: config.Config{ | ||||
| 				NoProxyCheck: utils.NewNoneOption[bool](), | ||||
| 			}, | ||||
| 		} | ||||
| 		dodosCount   uint | ||||
| 		requestCount uint | ||||
| 		timeout      uint32 | ||||
| 		noProxyCheck bool | ||||
| 		rootCmd      = &cobra.Command{ | ||||
| 			Use: "dodo [flags]", | ||||
| 			Example: `  Simple usage only with URL: | ||||
|     dodo -u https://example.com | ||||
|  | ||||
|   Simple usage with config file: | ||||
|     dodo -c /path/to/config/file/config.json | ||||
|  | ||||
|   Usage with all flags: | ||||
|     dodo -c /path/to/config/file/config.json -u https://example.com -m POST -d 10 -r 1000 -t 2000 --no-proxy-check -y`, | ||||
| 			Run:           func(cmd *cobra.Command, args []string) {}, | ||||
| 			SilenceErrors: true, | ||||
| 			SilenceUsage:  true, | ||||
| 			Version:       config.VERSION, | ||||
| 		} | ||||
| 	) | ||||
|  | ||||
| 	rootCmd.Flags().StringVarP(&cliConfig.ConfigFile, "config-file", "c", "", "Path to the config file") | ||||
| 	rootCmd.Flags().BoolVarP(&cliConfig.Yes, "yes", "y", false, "Answer yes to all questions") | ||||
| 	rootCmd.Flags().StringVarP(&cliConfig.Method, "method", "m", "", fmt.Sprintf("HTTP Method (default %s)", config.DefaultMethod)) | ||||
| 	rootCmd.Flags().StringVarP(&cliConfig.URL, "url", "u", "", "URL for stress testing") | ||||
| 	rootCmd.Flags().UintVarP(&dodosCount, "dodos-count", "d", config.DefaultDodosCount, "Number of dodos(threads)") | ||||
| 	rootCmd.Flags().UintVarP(&requestCount, "request-count", "r", config.DefaultRequestCount, "Number of total requests") | ||||
| 	rootCmd.Flags().Uint32VarP(&timeout, "timeout", "t", config.DefaultTimeout, "Timeout for each request in milliseconds") | ||||
| 	rootCmd.Flags().BoolVar(&noProxyCheck, "no-proxy-check", false, "Do not check for proxies") | ||||
| 	if err := rootCmd.Execute(); err != nil { | ||||
| 		utils.PrintErr(err) | ||||
| 		rootCmd.Println(rootCmd.UsageString()) | ||||
| 		return nil, customerrors.CobraErrorFormater(err) | ||||
| 	} | ||||
| 	rootCmd.Flags().Visit(func(f *pflag.Flag) { | ||||
| 		switch f.Name { | ||||
| 		case "help": | ||||
| 			returnNil = true | ||||
| 		case "version": | ||||
| 			returnNil = true | ||||
| 		case "dodos-count": | ||||
| 			cliConfig.DodosCount = dodosCount | ||||
| 		case "request-count": | ||||
| 			cliConfig.RequestCount = requestCount | ||||
| 		case "timeout": | ||||
| 			cliConfig.Timeout = timeout | ||||
| 		case "no-proxy-check": | ||||
| 			cliConfig.NoProxyCheck = utils.NewOption(noProxyCheck) | ||||
| 		} | ||||
| 	}) | ||||
| 	if returnNil { | ||||
| 		return nil, nil | ||||
| 	} | ||||
| 	return cliConfig, nil | ||||
| } | ||||
|  | ||||
| // CLIYesOrNoReader reads a yes or no answer from the command line. | ||||
| // It prompts the user with the given message and default value, | ||||
| // and returns true if the user answers "y" or "Y", and false otherwise. | ||||
| // If there is an error while reading the input, it returns false. | ||||
| // If the user simply presses enter without providing any input, | ||||
| // it returns the default value specified by the `dft` parameter. | ||||
| func CLIYesOrNoReader(message string, dft bool) bool { | ||||
| 	var answer string | ||||
| 	defaultMessage := "Y/n" | ||||
| 	if !dft { | ||||
| 		defaultMessage = "y/N" | ||||
| 	} | ||||
| 	fmt.Printf("%s [%s]: ", message, defaultMessage) | ||||
| 	if _, err := fmt.Scanln(&answer); err != nil { | ||||
| 		if err.Error() == "unexpected newline" { | ||||
| 			return dft | ||||
| 		} | ||||
| 		return false | ||||
| 	} | ||||
| 	if answer == "" { | ||||
| 		return dft | ||||
| 	} | ||||
| 	return answer == "y" || answer == "Y" | ||||
| } | ||||
							
								
								
									
										38
									
								
								readers/json.go
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										38
									
								
								readers/json.go
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,38 @@ | ||||
| package readers | ||||
|  | ||||
| import ( | ||||
| 	"encoding/json" | ||||
| 	"os" | ||||
|  | ||||
| 	"github.com/aykhans/dodo/config" | ||||
| 	customerrors "github.com/aykhans/dodo/custom_errors" | ||||
| 	"github.com/aykhans/dodo/utils" | ||||
| ) | ||||
|  | ||||
| func JSONConfigReader(filePath string) (*config.JSONConfig, error) { | ||||
| 	data, err := os.ReadFile(filePath) | ||||
| 	if err != nil { | ||||
| 		return nil, customerrors.OSErrorFormater(err) | ||||
| 	} | ||||
| 	jsonConf := &config.JSONConfig{ | ||||
| 		Config: config.Config{ | ||||
| 			NoProxyCheck: utils.NewNoneOption[bool](), | ||||
| 		}, | ||||
| 	} | ||||
| 	err = json.Unmarshal(data, &jsonConf) | ||||
|  | ||||
| 	if err != nil { | ||||
| 		switch err := err.(type) { | ||||
| 		case *json.UnmarshalTypeError: | ||||
| 			return nil, | ||||
| 				customerrors.NewTypeError( | ||||
| 					err.Type.String(), | ||||
| 					err.Value, | ||||
| 					err.Field, | ||||
| 					err, | ||||
| 				) | ||||
| 		} | ||||
| 		return nil, customerrors.NewInvalidFileError(filePath, err) | ||||
| 	} | ||||
| 	return jsonConf, nil | ||||
| } | ||||
| @@ -2,12 +2,14 @@ package requests | ||||
|  | ||||
| import ( | ||||
| 	"context" | ||||
| 	"crypto/tls" | ||||
| 	"errors" | ||||
| 	"fmt" | ||||
| 	"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" | ||||
| @@ -18,52 +20,87 @@ 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( | ||||
| 	_ context.Context, | ||||
| 	ctx context.Context, | ||||
| 	timeout time.Duration, | ||||
| 	proxies []url.URL, | ||||
| 	proxies []config.Proxy, | ||||
| 	dodosCount uint, | ||||
| 	maxConns uint, | ||||
| 	URL url.URL, | ||||
| 	skipVerify bool, | ||||
| 	yes bool, | ||||
| 	noProxyCheck bool, | ||||
| 	URL *url.URL, | ||||
| ) []*fasthttp.HostClient { | ||||
| 	isTLS := URL.Scheme == "https" | ||||
|  | ||||
| 	if proxiesLen := len(proxies); proxiesLen > 0 { | ||||
| 		clients := make([]*fasthttp.HostClient, 0, proxiesLen) | ||||
| 		addr := URL.Host | ||||
| 		if isTLS && URL.Port() == "" { | ||||
| 			addr += ":443" | ||||
| 		} | ||||
|  | ||||
| 		for _, proxy := range proxies { | ||||
| 			dialFunc, err := getDialFunc(&proxy, timeout) | ||||
| 			if err != nil { | ||||
| 				continue | ||||
| 		// 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() == "" { | ||||
| 				addr += ":443" | ||||
| 			} | ||||
|  | ||||
| 			clients = append(clients, &fasthttp.HostClient{ | ||||
| 				MaxConns: int(maxConns), | ||||
| 				IsTLS:    isTLS, | ||||
| 				TLSConfig: &tls.Config{ | ||||
| 					InsecureSkipVerify: skipVerify, | ||||
| 			for _, proxy := range proxies { | ||||
| 				dialFunc, err := getDialFunc(&proxy, timeout) | ||||
| 				if err != nil { | ||||
| 					continue | ||||
| 				} | ||||
|  | ||||
| 				clients = append(clients, &fasthttp.HostClient{ | ||||
| 					MaxConns:            int(maxConns), | ||||
| 					IsTLS:               isTLS, | ||||
| 					Addr:                addr, | ||||
| 					Dial:                dialFunc, | ||||
| 					MaxIdleConnDuration: timeout, | ||||
| 					MaxConnDuration:     timeout, | ||||
| 					WriteTimeout:        timeout, | ||||
| 					ReadTimeout:         timeout, | ||||
| 				}, | ||||
| 				Addr:                addr, | ||||
| 				Dial:                dialFunc, | ||||
| 				MaxIdleConnDuration: timeout, | ||||
| 				MaxConnDuration:     timeout, | ||||
| 				WriteTimeout:        timeout, | ||||
| 				ReadTimeout:         timeout, | ||||
| 			}, | ||||
| 				) | ||||
| 			} | ||||
| 			return clients | ||||
| 		} | ||||
|  | ||||
| 		// Else, we will check the proxies and return the active ones. | ||||
| 		activeProxyClients := getActiveProxyClients( | ||||
| 			ctx, proxies, timeout, dodosCount, maxConns, URL, | ||||
| 		) | ||||
| 		if ctx.Err() != nil { | ||||
| 			return nil | ||||
| 		} | ||||
| 		activeProxyClientsCount := uint(len(activeProxyClients)) | ||||
| 		var yesOrNoMessage string | ||||
| 		var yesOrNoDefault bool | ||||
| 		if activeProxyClientsCount == 0 { | ||||
| 			yesOrNoDefault = false | ||||
| 			yesOrNoMessage = utils.Colored( | ||||
| 				utils.Colors.Yellow, | ||||
| 				"No active proxies found. Do you want to continue?", | ||||
| 			) | ||||
| 		} else { | ||||
| 			yesOrNoMessage = utils.Colored( | ||||
| 				utils.Colors.Yellow, | ||||
| 				fmt.Sprintf( | ||||
| 					"Found %d active proxies. Do you want to continue?", | ||||
| 					activeProxyClientsCount, | ||||
| 				), | ||||
| 			) | ||||
| 		} | ||||
| 		return clients | ||||
| 		if !yes { | ||||
| 			response := readers.CLIYesOrNoReader("\n"+yesOrNoMessage, yesOrNoDefault) | ||||
| 			if !response { | ||||
| 				utils.PrintAndExit("Exiting...") | ||||
| 			} | ||||
| 		} | ||||
| 		fmt.Println() | ||||
| 		if activeProxyClientsCount > 0 { | ||||
| 			return activeProxyClients | ||||
| 		} | ||||
| 	} | ||||
|  | ||||
| 	client := &fasthttp.HostClient{ | ||||
| 		MaxConns: int(maxConns), | ||||
| 		IsTLS:    isTLS, | ||||
| 		TLSConfig: &tls.Config{ | ||||
| 			InsecureSkipVerify: skipVerify, | ||||
| 		}, | ||||
| 		MaxConns:            int(maxConns), | ||||
| 		IsTLS:               isTLS, | ||||
| 		Addr:                URL.Host, | ||||
| 		MaxIdleConnDuration: timeout, | ||||
| 		MaxConnDuration:     timeout, | ||||
| @@ -73,26 +110,201 @@ func getClients( | ||||
| 	return []*fasthttp.HostClient{client} | ||||
| } | ||||
|  | ||||
| // 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) { | ||||
| // 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 | ||||
| 	} | ||||
|  | ||||
| 	var dialer fasthttp.DialFunc | ||||
|  | ||||
| 	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") | ||||
| 	if parsedProxyURL.Scheme == "socks5" || parsedProxyURL.Scheme == "socks5h" { | ||||
| 		if proxy.Username != "" { | ||||
| 			dialer = fasthttpproxy.FasthttpSocksDialer( | ||||
| 				fmt.Sprintf( | ||||
| 					"%s://%s:%s@%s", | ||||
| 					parsedProxyURL.Scheme, | ||||
| 					proxy.Username, | ||||
| 					proxy.Password, | ||||
| 					parsedProxyURL.Host, | ||||
| 				), | ||||
| 			) | ||||
| 		} else { | ||||
| 			dialer = fasthttpproxy.FasthttpSocksDialer( | ||||
| 				fmt.Sprintf( | ||||
| 					"%s://%s", | ||||
| 					parsedProxyURL.Scheme, | ||||
| 					parsedProxyURL.Host, | ||||
| 				), | ||||
| 			) | ||||
| 		} | ||||
| 	} else if parsedProxyURL.Scheme == "http" { | ||||
| 		if proxy.Username != "" { | ||||
| 			dialer = fasthttpproxy.FasthttpHTTPDialerTimeout( | ||||
| 				fmt.Sprintf( | ||||
| 					"%s:%s@%s", | ||||
| 					proxy.Username, proxy.Password, parsedProxyURL.Host, | ||||
| 				), | ||||
| 				timeout, | ||||
| 			) | ||||
| 		} else { | ||||
| 			dialer = fasthttpproxy.FasthttpHTTPDialerTimeout( | ||||
| 				parsedProxyURL.Host, | ||||
| 				timeout, | ||||
| 			) | ||||
| 		} | ||||
| 	} else { | ||||
| 		return nil, err | ||||
| 	} | ||||
|  | ||||
| 	if dialer == nil { | ||||
| 		return nil, errors.New("internal error: proxy dialer is nil") | ||||
| 	} | ||||
|  | ||||
| 	return dialer, nil | ||||
| } | ||||
|  | ||||
|   | ||||
| @@ -7,6 +7,7 @@ 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. | ||||
| @@ -17,7 +18,7 @@ import ( | ||||
| func streamProgress( | ||||
| 	ctx context.Context, | ||||
| 	wg *sync.WaitGroup, | ||||
| 	total uint, | ||||
| 	total int64, | ||||
| 	message string, | ||||
| 	increase <-chan int64, | ||||
| ) { | ||||
| @@ -27,26 +28,19 @@ 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:   int64(total), | ||||
| 		Total:   total, | ||||
| 	} | ||||
| 	pw.AppendTracker(&dodosTracker) | ||||
|  | ||||
| 	for { | ||||
| 		select { | ||||
| 		case <-ctx.Done(): | ||||
| 			if err := ctx.Err(); err == context.Canceled || err == context.DeadlineExceeded { | ||||
| 				dodosTracker.MarkAsDone() | ||||
| 			} else { | ||||
| 				dodosTracker.MarkAsErrored() | ||||
| 			} | ||||
| 			time.Sleep(time.Millisecond * 300) | ||||
| 			fmt.Printf("\r") | ||||
| 			dodosTracker.MarkAsErrored() | ||||
| 			time.Sleep(time.Millisecond * 300) | ||||
| 			pw.Stop() | ||||
| 			return | ||||
|  | ||||
| 		case value := <-increase: | ||||
| @@ -54,3 +48,28 @@ func streamProgress( | ||||
| 		} | ||||
| 	} | ||||
| } | ||||
|  | ||||
| // checkConnection checks the internet connection by making requests to different websites. | ||||
| // It returns true if the connection is successful, otherwise false. | ||||
| func checkConnection(ctx context.Context) bool { | ||||
| 	ch := make(chan bool) | ||||
| 	go func() { | ||||
| 		_, _, err := fasthttp.Get(nil, "https://www.google.com") | ||||
| 		if err != nil { | ||||
| 			_, _, err = fasthttp.Get(nil, "https://www.bing.com") | ||||
| 			if err != nil { | ||||
| 				_, _, err = fasthttp.Get(nil, "https://www.yahoo.com") | ||||
| 				ch <- err == nil | ||||
| 			} | ||||
| 			ch <- true | ||||
| 		} | ||||
| 		ch <- true | ||||
| 	}() | ||||
|  | ||||
| 	select { | ||||
| 	case <-ctx.Done(): | ||||
| 		return false | ||||
| 	case res := <-ch: | ||||
| 		return res | ||||
| 	} | ||||
| } | ||||
|   | ||||
| @@ -1,15 +1,13 @@ | ||||
| package requests | ||||
|  | ||||
| import ( | ||||
| 	"bytes" | ||||
| 	"context" | ||||
| 	"math/rand" | ||||
| 	"net/url" | ||||
| 	"text/template" | ||||
| 	"time" | ||||
|  | ||||
| 	"github.com/aykhans/dodo/config" | ||||
| 	"github.com/aykhans/dodo/types" | ||||
| 	customerrors "github.com/aykhans/dodo/custom_errors" | ||||
| 	"github.com/aykhans/dodo/utils" | ||||
| 	"github.com/valyala/fasthttp" | ||||
| ) | ||||
| @@ -23,16 +21,12 @@ 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() | ||||
| @@ -50,9 +44,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, types.ErrTimeout | ||||
| 		return nil, customerrors.ErrTimeout | ||||
| 	case <-ctx.Done(): | ||||
| 		return nil, types.ErrInterrupt | ||||
| 		return nil, customerrors.ErrInterrupt | ||||
| 	} | ||||
| } | ||||
|  | ||||
| @@ -81,9 +75,9 @@ func newRequest( | ||||
|  | ||||
| 	getRequest := getRequestGeneratorFunc( | ||||
| 		requestConfig.URL, | ||||
| 		requestConfig.Params, | ||||
| 		requestConfig.Headers, | ||||
| 		requestConfig.Cookies, | ||||
| 		requestConfig.Params, | ||||
| 		requestConfig.Method, | ||||
| 		requestConfig.Body, | ||||
| 		localRand, | ||||
| @@ -97,39 +91,38 @@ 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, | ||||
| 	params types.Params, | ||||
| 	headers types.Headers, | ||||
| 	cookies types.Cookies, | ||||
| 	method string, | ||||
| 	bodies []string, | ||||
| 	URL *url.URL, | ||||
| 	Headers map[string][]string, | ||||
| 	Cookies map[string][]string, | ||||
| 	Params map[string][]string, | ||||
| 	Method string, | ||||
| 	Bodies []string, | ||||
| 	localRand *rand.Rand, | ||||
| ) RequestGeneratorFunc { | ||||
| 	getParams := getKeyValueGeneratorFunc(params, localRand) | ||||
| 	getHeaders := getKeyValueGeneratorFunc(headers, localRand) | ||||
| 	getCookies := getKeyValueGeneratorFunc(cookies, localRand) | ||||
| 	getBody := getBodyValueFunc(bodies, utils.NewFuncMapGenerator(localRand), localRand) | ||||
| 	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) | ||||
|  | ||||
| 	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, | ||||
| 			getParams(), | ||||
| 			headers, | ||||
| 			getHeaders(), | ||||
| 			getCookies(), | ||||
| 			method, | ||||
| 			body, | ||||
| 			getParams(), | ||||
| 			Method, | ||||
| 			getBody(), | ||||
| 		) | ||||
| 	} | ||||
| } | ||||
| @@ -137,12 +130,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, | ||||
| 	params []types.KeyValue[string, string], | ||||
| 	headers []types.KeyValue[string, string], | ||||
| 	cookies []types.KeyValue[string, string], | ||||
| 	method string, | ||||
| 	body string, | ||||
| 	URL *url.URL, | ||||
| 	Headers map[string]string, | ||||
| 	Cookies map[string]string, | ||||
| 	Params map[string]string, | ||||
| 	Method string, | ||||
| 	Body string, | ||||
| ) *fasthttp.Request { | ||||
| 	request := fasthttp.AcquireRequest() | ||||
| 	request.SetRequestURI(URL.Path) | ||||
| @@ -150,12 +143,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.SetHost(URL.Host) | ||||
| 	setRequestParams(request, params) | ||||
| 	setRequestHeaders(request, headers) | ||||
| 	setRequestCookies(request, cookies) | ||||
| 	setRequestMethod(request, method) | ||||
| 	setRequestBody(request, body) | ||||
| 	request.Header.Set("Host", URL.Host) | ||||
| 	setRequestHeaders(request, Headers) | ||||
| 	setRequestCookies(request, Cookies) | ||||
| 	setRequestParams(request, Params) | ||||
| 	setRequestMethod(request, Method) | ||||
| 	setRequestBody(request, Body) | ||||
| 	if URL.Scheme == "https" { | ||||
| 		request.URI().SetScheme("https") | ||||
| 	} | ||||
| @@ -163,25 +156,28 @@ func newFasthttpRequest( | ||||
| 	return request | ||||
| } | ||||
|  | ||||
| // 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) | ||||
| // 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) | ||||
| 	} | ||||
| } | ||||
|  | ||||
| // 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) | ||||
| // 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) | ||||
| 	} | ||||
| } | ||||
|  | ||||
| // 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) | ||||
| // 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) | ||||
| 	} | ||||
| 	req.URI().SetQueryString(urlParams.Encode()) | ||||
| } | ||||
|  | ||||
| // setRequestMethod sets the HTTP request method for the given request. | ||||
| @@ -195,147 +191,59 @@ func setRequestBody(req *fasthttp.Request, body string) { | ||||
| 	req.SetBody([]byte(body)) | ||||
| } | ||||
|  | ||||
| // 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. | ||||
| // 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. | ||||
| // | ||||
| // 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)) | ||||
| // 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) | ||||
|  | ||||
| 	funcMap := *utils.NewFuncMapGenerator(localRand).GetFuncMap() | ||||
|  | ||||
| 	for i, kv := range keyValueSlice { | ||||
| 		keyValueGenerators[i] = keyValueGenerator{ | ||||
| 			key:   getKeyFunc(kv.Key, funcMap), | ||||
| 			value: getValueFunc(kv.Value, funcMap, localRand), | ||||
| 		// 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 | ||||
| 		} | ||||
|  | ||||
| 		getKeyValueSlice = append( | ||||
| 			getKeyValueSlice, | ||||
| 			map[string]func() string{key: getKeyValue}, | ||||
| 		) | ||||
| 	} | ||||
|  | ||||
| 	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(), | ||||
| 	// 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 keyValues | ||||
| 		} | ||||
| 	} else { | ||||
| 		keyValues := make(KeyValue, len(getKeyValueSlice)) | ||||
| 		for _, keyValue := range getKeyValueSlice { | ||||
| 			for key, value := range keyValue { | ||||
| 				keyValues[key] = value() | ||||
| 			} | ||||
| 		} | ||||
| 		return keyValues | ||||
| 	} | ||||
| } | ||||
|  | ||||
| // getKeyFunc creates a function that processes a key string through Go's template engine. | ||||
| // It takes a key string and a template.FuncMap containing the available template functions. | ||||
| // | ||||
| // The returned function, when called, will execute the template with the given key and return | ||||
| // the processed string result. If template parsing fails, the returned function will always | ||||
| // return an empty string. | ||||
| // | ||||
| // This enables dynamic generation of keys that can include template directives and functions. | ||||
| func getKeyFunc(key string, funcMap template.FuncMap) func() string { | ||||
| 	t, err := template.New("default").Funcs(funcMap).Parse(key) | ||||
| 	if err != nil { | ||||
| 		return func() string { return "" } | ||||
| 	} | ||||
|  | ||||
| 	return func() string { | ||||
| 		var buf bytes.Buffer | ||||
| 		_ = t.Execute(&buf, nil) | ||||
| 		return buf.String() | ||||
| 	} | ||||
| } | ||||
|  | ||||
| // getValueFunc creates a function that randomly selects and processes a value from a slice of strings | ||||
| // through Go's template engine. | ||||
| // | ||||
| // Parameters: | ||||
| //   - values: A slice of string templates that can contain template directives | ||||
| //   - funcMap: A template.FuncMap containing all available template functions | ||||
| //   - localRand: A random number generator for consistent randomization | ||||
| // | ||||
| // The returned function, when called, will: | ||||
| //  1. Select a random template from the values slice | ||||
| //  2. Execute the selected template | ||||
| //  3. Return the processed string result | ||||
| // | ||||
| // If a selected template is nil (due to earlier parsing failure), the function will return an empty string. | ||||
| // This enables dynamic generation of values with randomized selection from multiple templates. | ||||
| func getValueFunc( | ||||
| 	values []string, | ||||
| 	funcMap template.FuncMap, | ||||
| 	localRand *rand.Rand, | ||||
| ) func() string { | ||||
| 	templates := make([]*template.Template, len(values)) | ||||
|  | ||||
| 	for i, value := range values { | ||||
| 		t, err := template.New("default").Funcs(funcMap).Parse(value) | ||||
| 		if err != nil { | ||||
| 			templates[i] = nil | ||||
| 		} | ||||
| 		templates[i] = t | ||||
| 	} | ||||
|  | ||||
| 	randomTemplateFunc := utils.RandomValueCycle(templates, localRand) | ||||
|  | ||||
| 	return func() string { | ||||
| 		if tmpl := randomTemplateFunc(); tmpl == nil { | ||||
| 			return "" | ||||
| 		} else { | ||||
| 			var buf bytes.Buffer | ||||
| 			_ = tmpl.Execute(&buf, nil) | ||||
| 			return buf.String() | ||||
| 		} | ||||
| 	} | ||||
| } | ||||
|  | ||||
| // getBodyValueFunc creates a function that randomly selects and processes a request body from a slice of templates. | ||||
| // It returns a closure that generates both the body content and the appropriate Content-Type header value. | ||||
| // | ||||
| // Parameters: | ||||
| //   - values: A slice of string templates that can contain template directives for request bodies | ||||
| //   - funcMapGenerator: Provides template functions and content type information | ||||
| //   - localRand: A random number generator for consistent randomization | ||||
| // | ||||
| // The returned function, when called, will: | ||||
| //  1. Select a random body template from the values slice | ||||
| //  2. Execute the selected template with available template functions | ||||
| //  3. Return both the processed body string and the appropriate Content-Type header value | ||||
| // | ||||
| // If the selected template is nil (due to earlier parsing failure), the function will return | ||||
| // empty strings for both the body and Content-Type. | ||||
| // | ||||
| // This enables dynamic generation of request bodies with proper content type headers. | ||||
| func getBodyValueFunc( | ||||
| 	values []string, | ||||
| 	funcMapGenerator *utils.FuncMapGenerator, | ||||
| 	localRand *rand.Rand, | ||||
| ) func() (string, string) { | ||||
| 	templates := make([]*template.Template, len(values)) | ||||
|  | ||||
| 	for i, value := range values { | ||||
| 		t, err := template.New("default").Funcs(*funcMapGenerator.GetFuncMap()).Parse(value) | ||||
| 		if err != nil { | ||||
| 			templates[i] = nil | ||||
| 		} | ||||
| 		templates[i] = t | ||||
| 	} | ||||
|  | ||||
| 	randomTemplateFunc := utils.RandomValueCycle(templates, localRand) | ||||
|  | ||||
| 	return func() (string, string) { | ||||
| 		if tmpl := randomTemplateFunc(); tmpl == nil { | ||||
| 			return "", "" | ||||
| 		} else { | ||||
| 			var buf bytes.Buffer | ||||
| 			_ = tmpl.Execute(&buf, nil) | ||||
| 			return buf.String(), funcMapGenerator.GetBodyDataHeader() | ||||
| 		} | ||||
| 		return func() KeyValue { return keyValues } | ||||
| 	} | ||||
| } | ||||
|   | ||||
| @@ -1,94 +1,85 @@ | ||||
| package requests | ||||
|  | ||||
| import ( | ||||
| 	"fmt" | ||||
| 	"os" | ||||
| 	"time" | ||||
|  | ||||
| 	"github.com/aykhans/dodo/types" | ||||
| 	"github.com/aykhans/dodo/utils" | ||||
| 	"github.com/jedib0t/go-pretty/v6/table" | ||||
| ) | ||||
|  | ||||
| type Response struct { | ||||
| 	Response string | ||||
| 	Time     time.Duration | ||||
| 	StatusCode int | ||||
| 	Error      error | ||||
| 	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() { | ||||
| 	if len(responses) == 0 { | ||||
| 		return | ||||
| 	} | ||||
| // response count, minimum time, maximum time, and average time. | ||||
| func (respones Responses) Print() { | ||||
| 	var ( | ||||
| 		totalMinDuration time.Duration = respones[0].Time | ||||
| 		totalMaxDuration time.Duration = respones[0].Time | ||||
| 		totalDuration    time.Duration | ||||
| 		totalCount       int = len(respones) | ||||
| 	) | ||||
| 	mergedResponses := make(map[string][]time.Duration) | ||||
|  | ||||
| 	mergedResponses := make(map[string]types.Durations) | ||||
| 	for _, response := range respones { | ||||
| 		if response.Time < totalMinDuration { | ||||
| 			totalMinDuration = response.Time | ||||
| 		} | ||||
| 		if response.Time > totalMaxDuration { | ||||
| 			totalMaxDuration = response.Time | ||||
| 		} | ||||
| 		totalDuration += response.Time | ||||
|  | ||||
| 	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, | ||||
| 		) | ||||
| 		if response.Error != nil { | ||||
| 			mergedResponses[response.Error.Error()] = append( | ||||
| 				mergedResponses[response.Error.Error()], | ||||
| 				response.Time, | ||||
| 			) | ||||
| 		} else { | ||||
| 			mergedResponses[fmt.Sprintf("%d", response.StatusCode)] = append( | ||||
| 				mergedResponses[fmt.Sprintf("%d", response.StatusCode)], | ||||
| 				response.Time, | ||||
| 			) | ||||
| 		} | ||||
| 	} | ||||
|  | ||||
| 	t := table.NewWriter() | ||||
| 	t.SetOutputMirror(os.Stdout) | ||||
| 	t.SetStyle(table.StyleLight) | ||||
| 	t.SetColumnConfigs([]table.ColumnConfig{ | ||||
| 		{Number: 1, WidthMax: 40}, | ||||
| 		{Number: 1, WidthMax: 80}, | ||||
| 	}) | ||||
|  | ||||
| 	t.AppendHeader(table.Row{ | ||||
| 		"Response", | ||||
| 		"Count", | ||||
| 		"Min", | ||||
| 		"Max", | ||||
| 		"Average", | ||||
| 		"P90", | ||||
| 		"P95", | ||||
| 		"P99", | ||||
| 		"Min Time", | ||||
| 		"Max Time", | ||||
| 		"Average Time", | ||||
| 	}) | ||||
|  | ||||
| 	var roundPrecision int64 = 4 | ||||
| 	for key, durations := range mergedResponses { | ||||
| 		durations.Sort() | ||||
| 		durationsLen := len(durations) | ||||
| 		durationsLenAsFloat := float64(durationsLen - 1) | ||||
|  | ||||
| 		t.AppendRow(table.Row{ | ||||
| 			key, | ||||
| 			durationsLen, | ||||
| 			utils.DurationRoundBy(*durations.First(), roundPrecision), | ||||
| 			utils.DurationRoundBy(*durations.Last(), roundPrecision), | ||||
| 			utils.DurationRoundBy(durations.Avg(), roundPrecision), | ||||
| 			utils.DurationRoundBy(durations[int(0.90*durationsLenAsFloat)], roundPrecision), | ||||
| 			utils.DurationRoundBy(durations[int(0.95*durationsLenAsFloat)], roundPrecision), | ||||
| 			utils.DurationRoundBy(durations[int(0.99*durationsLenAsFloat)], roundPrecision), | ||||
| 			len(durations), | ||||
| 			utils.MinDuration(durations...), | ||||
| 			utils.MaxDuration(durations...), | ||||
| 			utils.AvgDuration(durations...), | ||||
| 		}) | ||||
| 		t.AppendSeparator() | ||||
| 	} | ||||
|  | ||||
| 	if len(mergedResponses) > 1 { | ||||
| 		totalDurations.Sort() | ||||
| 		allDurationsLenAsFloat := float64(len(totalDurations) - 1) | ||||
|  | ||||
| 		t.AppendRow(table.Row{ | ||||
| 			"Total", | ||||
| 			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.AppendRow(table.Row{ | ||||
| 		"Total", | ||||
| 		totalCount, | ||||
| 		totalMinDuration, | ||||
| 		totalMaxDuration, | ||||
| 		totalDuration / time.Duration(totalCount), | ||||
| 	}) | ||||
| 	t.Render() | ||||
| } | ||||
|   | ||||
							
								
								
									
										156
									
								
								requests/run.go
									
									
									
									
									
								
							
							
						
						
									
										156
									
								
								requests/run.go
									
									
									
									
									
								
							| @@ -2,45 +2,52 @@ package requests | ||||
|  | ||||
| import ( | ||||
| 	"context" | ||||
| 	"strconv" | ||||
| 	"sync" | ||||
| 	"time" | ||||
|  | ||||
| 	"github.com/aykhans/dodo/config" | ||||
| 	"github.com/aykhans/dodo/types" | ||||
| 	customerrors "github.com/aykhans/dodo/custom_errors" | ||||
| 	"github.com/aykhans/dodo/utils" | ||||
| 	"github.com/valyala/fasthttp" | ||||
| ) | ||||
|  | ||||
| // Run executes the main logic for processing requests based on the provided configuration. | ||||
| // It initializes clients based on the request configuration and releases the dodos. | ||||
| // If the context is canceled and no responses are collected, it returns an interrupt error. | ||||
| // 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. | ||||
| // | ||||
| // 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) { | ||||
| 	if requestConfig.Duration > 0 { | ||||
| 		var cancel context.CancelFunc | ||||
| 		ctx, cancel = context.WithTimeout(ctx, requestConfig.Duration) | ||||
| 		defer cancel() | ||||
| 	checkConnectionCtx, checkConnectionCtxCancel := context.WithTimeout(ctx, 8*time.Second) | ||||
| 	if !checkConnection(checkConnectionCtx) { | ||||
| 		checkConnectionCtxCancel() | ||||
| 		return nil, customerrors.ErrNoInternet | ||||
| 	} | ||||
| 	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, types.ErrInterrupt | ||||
| 		return nil, customerrors.ErrInterrupt | ||||
| 	} | ||||
|  | ||||
| 	responses := releaseDodos(ctx, requestConfig, clients) | ||||
| 	if ctx.Err() != nil && len(responses) == 0 { | ||||
| 		return nil, types.ErrInterrupt | ||||
| 		return nil, customerrors.ErrInterrupt | ||||
| 	} | ||||
|  | ||||
| 	return responses, nil | ||||
| @@ -65,65 +72,53 @@ func releaseDodos( | ||||
| 		wg                  sync.WaitGroup | ||||
| 		streamWG            sync.WaitGroup | ||||
| 		requestCountPerDodo uint | ||||
| 		dodosCount          = requestConfig.GetValidDodosCountForRequests() | ||||
| 		responses           = make([][]Response, dodosCount) | ||||
| 		increase            = make(chan int64, requestConfig.RequestCount) | ||||
| 		dodosCount          uint = requestConfig.GetValidDodosCountForRequests() | ||||
| 		dodosCountInt       int  = int(dodosCount) | ||||
| 		requestCount        uint = uint(requestConfig.RequestCount) | ||||
| 		responses                = make([][]*Response, dodosCount) | ||||
| 		increase                 = make(chan int64, requestCount) | ||||
| 	) | ||||
|  | ||||
| 	wg.Add(int(dodosCount)) | ||||
| 	wg.Add(dodosCountInt) | ||||
| 	streamWG.Add(1) | ||||
| 	streamCtx, streamCtxCancel := context.WithCancel(ctx) | ||||
| 	streamCtx, streamCtxCancel := context.WithCancel(context.Background()) | ||||
|  | ||||
| 	go streamProgress(streamCtx, &streamWG, requestConfig.RequestCount, "Dodos Working🔥", increase) | ||||
| 	go streamProgress(streamCtx, &streamWG, int64(requestCount), "Dodos Working🔥", increase) | ||||
|  | ||||
| 	if requestConfig.RequestCount == 0 { | ||||
| 		for i := range dodosCount { | ||||
| 			go sendRequest( | ||||
| 				ctx, | ||||
| 				newRequest(*requestConfig, clients, int64(i)), | ||||
| 				requestConfig.Timeout, | ||||
| 				&responses[i], | ||||
| 				increase, | ||||
| 				&wg, | ||||
| 			) | ||||
| 	for i := range dodosCount { | ||||
| 		if i+1 == dodosCount { | ||||
| 			requestCountPerDodo = requestCount - (i * requestCount / dodosCount) | ||||
| 		} else { | ||||
| 			requestCountPerDodo = ((i + 1) * requestCount / dodosCount) - | ||||
| 				(i * requestCount / dodosCount) | ||||
| 		} | ||||
| 	} else { | ||||
| 		for i := range dodosCount { | ||||
| 			if i+1 == dodosCount { | ||||
| 				requestCountPerDodo = requestConfig.RequestCount - (i * requestConfig.RequestCount / dodosCount) | ||||
| 			} else { | ||||
| 				requestCountPerDodo = ((i + 1) * requestConfig.RequestCount / dodosCount) - | ||||
| 					(i * requestConfig.RequestCount / dodosCount) | ||||
| 			} | ||||
|  | ||||
| 			go sendRequestByCount( | ||||
| 				ctx, | ||||
| 				newRequest(*requestConfig, clients, int64(i)), | ||||
| 				requestConfig.Timeout, | ||||
| 				requestCountPerDodo, | ||||
| 				&responses[i], | ||||
| 				increase, | ||||
| 				&wg, | ||||
| 			) | ||||
| 		} | ||||
| 		go sendRequest( | ||||
| 			ctx, | ||||
| 			newRequest(*requestConfig, clients, int64(i)), | ||||
| 			requestConfig.Timeout, | ||||
| 			requestCountPerDodo, | ||||
| 			&responses[i], | ||||
| 			increase, | ||||
| 			&wg, | ||||
| 		) | ||||
| 	} | ||||
|  | ||||
| 	wg.Wait() | ||||
| 	streamCtxCancel() | ||||
| 	streamWG.Wait() | ||||
| 	return utils.Flatten(responses) | ||||
| } | ||||
|  | ||||
| // sendRequestByCount sends a specified number of HTTP requests concurrently with a given timeout. | ||||
| // sendRequest sends a specified number of HTTP requests concurrently with a given timeout. | ||||
| // It appends the responses to the provided responseData slice and sends the count of completed requests | ||||
| // to the increase channel. The function terminates early if the context is canceled or if a custom | ||||
| // interrupt error is encountered. | ||||
| func sendRequestByCount( | ||||
| func sendRequest( | ||||
| 	ctx context.Context, | ||||
| 	request *Request, | ||||
| 	timeout time.Duration, | ||||
| 	requestCount uint, | ||||
| 	responseData *[]Response, | ||||
| 	responseData *[]*Response, | ||||
| 	increase chan<- int64, | ||||
| 	wg *sync.WaitGroup, | ||||
| ) { | ||||
| @@ -143,67 +138,22 @@ func sendRequestByCount( | ||||
| 			} | ||||
|  | ||||
| 			if err != nil { | ||||
| 				if err == types.ErrInterrupt { | ||||
| 				if err == customerrors.ErrInterrupt { | ||||
| 					return | ||||
| 				} | ||||
| 				*responseData = append(*responseData, Response{ | ||||
| 					Response: err.Error(), | ||||
| 					Time:     completedTime, | ||||
| 				*responseData = append(*responseData, &Response{ | ||||
| 					StatusCode: 0, | ||||
| 					Error:      err, | ||||
| 					Time:       completedTime, | ||||
| 				}) | ||||
| 				increase <- 1 | ||||
| 				return | ||||
| 			} | ||||
|  | ||||
| 			*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, | ||||
| 			*responseData = append(*responseData, &Response{ | ||||
| 				StatusCode: response.StatusCode(), | ||||
| 				Error:      nil, | ||||
| 				Time:       completedTime, | ||||
| 			}) | ||||
| 			increase <- 1 | ||||
| 		}() | ||||
|   | ||||
| @@ -1,94 +0,0 @@ | ||||
| package types | ||||
|  | ||||
| import ( | ||||
| 	"bytes" | ||||
| 	"encoding/json" | ||||
| 	"fmt" | ||||
|  | ||||
| 	"github.com/jedib0t/go-pretty/v6/text" | ||||
| ) | ||||
|  | ||||
| type Body []string | ||||
|  | ||||
| func (body Body) String() string { | ||||
| 	var buffer bytes.Buffer | ||||
| 	if len(body) == 0 { | ||||
| 		return buffer.String() | ||||
| 	} | ||||
|  | ||||
| 	if len(body) == 1 { | ||||
| 		buffer.WriteString(body[0]) | ||||
| 		return buffer.String() | ||||
| 	} | ||||
|  | ||||
| 	buffer.WriteString(text.FgBlue.Sprint("Random") + "[\n") | ||||
|  | ||||
| 	indent := "  " | ||||
|  | ||||
| 	displayLimit := 5 | ||||
|  | ||||
| 	for i, item := range body[:min(len(body), displayLimit)] { | ||||
| 		if i > 0 { | ||||
| 			buffer.WriteString(",\n") | ||||
| 		} | ||||
|  | ||||
| 		buffer.WriteString(indent + item) | ||||
| 	} | ||||
|  | ||||
| 	// Add remaining count if there are more items | ||||
| 	if remainingValues := len(body) - displayLimit; remainingValues > 0 { | ||||
| 		buffer.WriteString(",\n" + indent + text.FgGreen.Sprintf("+%d bodies", remainingValues)) | ||||
| 	} | ||||
|  | ||||
| 	buffer.WriteString("\n]") | ||||
| 	return buffer.String() | ||||
| } | ||||
|  | ||||
| func (body *Body) UnmarshalJSON(b []byte) error { | ||||
| 	var data any | ||||
| 	if err := json.Unmarshal(b, &data); err != nil { | ||||
| 		return err | ||||
| 	} | ||||
|  | ||||
| 	switch v := data.(type) { | ||||
| 	case string: | ||||
| 		*body = []string{v} | ||||
| 	case []any: | ||||
| 		var slice []string | ||||
| 		for _, item := range v { | ||||
| 			slice = append(slice, fmt.Sprintf("%v", item)) | ||||
| 		} | ||||
| 		*body = slice | ||||
| 	default: | ||||
| 		return fmt.Errorf("invalid type for Body: %T (should be string or []string)", v) | ||||
| 	} | ||||
|  | ||||
| 	return nil | ||||
| } | ||||
|  | ||||
| func (body *Body) UnmarshalYAML(unmarshal func(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 | ||||
| } | ||||
| @@ -1,32 +0,0 @@ | ||||
| package types | ||||
|  | ||||
| import "strings" | ||||
|  | ||||
| type FileLocationType int | ||||
|  | ||||
| const ( | ||||
| 	FileLocationTypeLocal FileLocationType = iota | ||||
| 	FileLocationTypeRemoteHTTP | ||||
| ) | ||||
|  | ||||
| type ConfigFile string | ||||
|  | ||||
| func (configFile ConfigFile) String() string { | ||||
| 	return string(configFile) | ||||
| } | ||||
|  | ||||
| func (configFile ConfigFile) LocationType() FileLocationType { | ||||
| 	if strings.HasPrefix(string(configFile), "http://") || strings.HasPrefix(string(configFile), "https://") { | ||||
| 		return FileLocationTypeRemoteHTTP | ||||
| 	} | ||||
| 	return FileLocationTypeLocal | ||||
| } | ||||
|  | ||||
| func (configFile ConfigFile) Extension() string { | ||||
| 	i := strings.LastIndex(configFile.String(), ".") | ||||
| 	if i == -1 { | ||||
| 		return "" | ||||
| 	} | ||||
|  | ||||
| 	return configFile.String()[i+1:] | ||||
| } | ||||
							
								
								
									
										139
									
								
								types/cookies.go
									
									
									
									
									
								
							
							
						
						
									
										139
									
								
								types/cookies.go
									
									
									
									
									
								
							| @@ -1,139 +0,0 @@ | ||||
| package types | ||||
|  | ||||
| import ( | ||||
| 	"bytes" | ||||
| 	"encoding/json" | ||||
| 	"fmt" | ||||
| 	"strings" | ||||
|  | ||||
| 	"github.com/jedib0t/go-pretty/v6/text" | ||||
| ) | ||||
|  | ||||
| type Cookies []KeyValue[string, []string] | ||||
|  | ||||
| func (cookies Cookies) String() string { | ||||
| 	var buffer bytes.Buffer | ||||
| 	if len(cookies) == 0 { | ||||
| 		return buffer.String() | ||||
| 	} | ||||
|  | ||||
| 	indent := "  " | ||||
|  | ||||
| 	displayLimit := 3 | ||||
|  | ||||
| 	for i, item := range cookies[:min(len(cookies), displayLimit)] { | ||||
| 		if i > 0 { | ||||
| 			buffer.WriteString(",\n") | ||||
| 		} | ||||
|  | ||||
| 		if len(item.Value) == 1 { | ||||
| 			buffer.WriteString(item.Key + ": " + item.Value[0]) | ||||
| 			continue | ||||
| 		} | ||||
| 		buffer.WriteString(item.Key + ": " + text.FgBlue.Sprint("Random") + "[\n") | ||||
|  | ||||
| 		for ii, v := range item.Value[:min(len(item.Value), displayLimit)] { | ||||
| 			if ii == len(item.Value)-1 { | ||||
| 				buffer.WriteString(indent + v + "\n") | ||||
| 			} else { | ||||
| 				buffer.WriteString(indent + v + ",\n") | ||||
| 			} | ||||
| 		} | ||||
|  | ||||
| 		// Add remaining values count if needed | ||||
| 		if remainingValues := len(item.Value) - displayLimit; remainingValues > 0 { | ||||
| 			buffer.WriteString(indent + text.FgGreen.Sprintf("+%d values", remainingValues) + "\n") | ||||
| 		} | ||||
|  | ||||
| 		buffer.WriteString("]") | ||||
| 	} | ||||
|  | ||||
| 	// Add remaining key-value pairs count if needed | ||||
| 	if remainingPairs := len(cookies) - displayLimit; remainingPairs > 0 { | ||||
| 		buffer.WriteString(",\n" + text.FgGreen.Sprintf("+%d cookies", remainingPairs)) | ||||
| 	} | ||||
|  | ||||
| 	return buffer.String() | ||||
| } | ||||
|  | ||||
| func (cookies *Cookies) AppendByKey(key, value string) { | ||||
| 	if item := cookies.GetValue(key); item != nil { | ||||
| 		*item = append(*item, value) | ||||
| 	} else { | ||||
| 		*cookies = append(*cookies, KeyValue[string, []string]{Key: key, Value: []string{value}}) | ||||
| 	} | ||||
| } | ||||
|  | ||||
| func (cookies Cookies) GetValue(key string) *[]string { | ||||
| 	for i := range cookies { | ||||
| 		if cookies[i].Key == key { | ||||
| 			return &cookies[i].Value | ||||
| 		} | ||||
| 	} | ||||
| 	return nil | ||||
| } | ||||
|  | ||||
| func (cookies *Cookies) UnmarshalJSON(b []byte) error { | ||||
| 	var data []map[string]any | ||||
| 	if err := json.Unmarshal(b, &data); err != nil { | ||||
| 		return err | ||||
| 	} | ||||
|  | ||||
| 	for _, item := range data { | ||||
| 		for key, value := range item { | ||||
| 			switch parsedValue := value.(type) { | ||||
| 			case string: | ||||
| 				*cookies = append(*cookies, KeyValue[string, []string]{Key: key, Value: []string{parsedValue}}) | ||||
| 			case []any: | ||||
| 				parsedStr := make([]string, len(parsedValue)) | ||||
| 				for i, item := range parsedValue { | ||||
| 					parsedStr[i] = fmt.Sprintf("%v", item) | ||||
| 				} | ||||
| 				*cookies = append(*cookies, KeyValue[string, []string]{Key: key, Value: parsedStr}) | ||||
| 			default: | ||||
| 				return fmt.Errorf("unsupported type for cookies expected string or []string, got %T", parsedValue) | ||||
| 			} | ||||
| 		} | ||||
| 	} | ||||
|  | ||||
| 	return nil | ||||
| } | ||||
|  | ||||
| func (cookies *Cookies) UnmarshalYAML(unmarshal func(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 | ||||
| } | ||||
| @@ -1,57 +0,0 @@ | ||||
| package types | ||||
|  | ||||
| import ( | ||||
| 	"encoding/json" | ||||
| 	"errors" | ||||
| 	"time" | ||||
| ) | ||||
|  | ||||
| type Duration struct { | ||||
| 	time.Duration | ||||
| } | ||||
|  | ||||
| func (duration *Duration) UnmarshalJSON(b []byte) error { | ||||
| 	var v any | ||||
| 	if err := json.Unmarshal(b, &v); err != nil { | ||||
| 		return err | ||||
| 	} | ||||
| 	switch value := v.(type) { | ||||
| 	case float64: | ||||
| 		duration.Duration = time.Duration(value) | ||||
| 		return nil | ||||
| 	case string: | ||||
| 		var err error | ||||
| 		duration.Duration, err = time.ParseDuration(value) | ||||
| 		if err != nil { | ||||
| 			return errors.New("Duration is invalid (e.g. 400ms, 1s, 5m, 1h)") | ||||
| 		} | ||||
| 		return nil | ||||
| 	default: | ||||
| 		return errors.New("Duration is invalid (e.g. 400ms, 1s, 5m, 1h)") | ||||
| 	} | ||||
| } | ||||
|  | ||||
| func (duration Duration) MarshalJSON() ([]byte, error) { | ||||
| 	return json.Marshal(duration.String()) | ||||
| } | ||||
|  | ||||
| func (duration *Duration) UnmarshalYAML(unmarshal func(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,40 +0,0 @@ | ||||
| package types | ||||
|  | ||||
| import ( | ||||
| 	"slices" | ||||
| 	"sort" | ||||
| 	"time" | ||||
| ) | ||||
|  | ||||
| type Durations []time.Duration | ||||
|  | ||||
| func (d Durations) Sort(ascending ...bool) { | ||||
| 	// If ascending is provided and is false, sort in descending order | ||||
| 	if len(ascending) > 0 && ascending[0] == false { | ||||
| 		sort.Slice(d, func(i, j int) bool { | ||||
| 			return d[i] > d[j] | ||||
| 		}) | ||||
| 	} else { // Otherwise, sort in ascending order | ||||
| 		slices.Sort(d) | ||||
| 	} | ||||
| } | ||||
|  | ||||
| func (d Durations) First() *time.Duration { | ||||
| 	return &d[0] | ||||
| } | ||||
|  | ||||
| func (d Durations) Last() *time.Duration { | ||||
| 	return &d[len(d)-1] | ||||
| } | ||||
|  | ||||
| func (d Durations) Sum() time.Duration { | ||||
| 	sum := time.Duration(0) | ||||
| 	for _, duration := range d { | ||||
| 		sum += duration | ||||
| 	} | ||||
| 	return sum | ||||
| } | ||||
|  | ||||
| func (d Durations) Avg() time.Duration { | ||||
| 	return d.Sum() / time.Duration(len(d)) | ||||
| } | ||||
| @@ -1,10 +0,0 @@ | ||||
| package types | ||||
|  | ||||
| import ( | ||||
| 	"errors" | ||||
| ) | ||||
|  | ||||
| var ( | ||||
| 	ErrInterrupt = errors.New("interrupted") | ||||
| 	ErrTimeout   = errors.New("timeout") | ||||
| ) | ||||
							
								
								
									
										156
									
								
								types/headers.go
									
									
									
									
									
								
							
							
						
						
									
										156
									
								
								types/headers.go
									
									
									
									
									
								
							| @@ -1,156 +0,0 @@ | ||||
| package types | ||||
|  | ||||
| import ( | ||||
| 	"bytes" | ||||
| 	"encoding/json" | ||||
| 	"fmt" | ||||
| 	"strings" | ||||
|  | ||||
| 	"github.com/jedib0t/go-pretty/v6/text" | ||||
| ) | ||||
|  | ||||
| type Headers []KeyValue[string, []string] | ||||
|  | ||||
| func (headers Headers) String() string { | ||||
| 	var buffer bytes.Buffer | ||||
| 	if len(headers) == 0 { | ||||
| 		return buffer.String() | ||||
| 	} | ||||
|  | ||||
| 	indent := "  " | ||||
|  | ||||
| 	displayLimit := 3 | ||||
|  | ||||
| 	for i, item := range headers[:min(len(headers), displayLimit)] { | ||||
| 		if i > 0 { | ||||
| 			buffer.WriteString(",\n") | ||||
| 		} | ||||
|  | ||||
| 		if len(item.Value) == 1 { | ||||
| 			buffer.WriteString(item.Key + ": " + item.Value[0]) | ||||
| 			continue | ||||
| 		} | ||||
| 		buffer.WriteString(item.Key + ": " + text.FgBlue.Sprint("Random") + "[\n") | ||||
|  | ||||
| 		for ii, v := range item.Value[:min(len(item.Value), displayLimit)] { | ||||
| 			if ii == len(item.Value)-1 { | ||||
| 				buffer.WriteString(indent + v + "\n") | ||||
| 			} else { | ||||
| 				buffer.WriteString(indent + v + ",\n") | ||||
| 			} | ||||
| 		} | ||||
|  | ||||
| 		// Add remaining values count if needed | ||||
| 		if remainingValues := len(item.Value) - displayLimit; remainingValues > 0 { | ||||
| 			buffer.WriteString(indent + text.FgGreen.Sprintf("+%d values", remainingValues) + "\n") | ||||
| 		} | ||||
|  | ||||
| 		buffer.WriteString("]") | ||||
| 	} | ||||
|  | ||||
| 	// Add remaining key-value pairs count if needed | ||||
| 	if remainingPairs := len(headers) - displayLimit; remainingPairs > 0 { | ||||
| 		buffer.WriteString(",\n" + text.FgGreen.Sprintf("+%d headers", remainingPairs)) | ||||
| 	} | ||||
|  | ||||
| 	return buffer.String() | ||||
| } | ||||
|  | ||||
| func (headers *Headers) AppendByKey(key, value string) { | ||||
| 	if item := headers.GetValue(key); item != nil { | ||||
| 		*item = append(*item, value) | ||||
| 	} else { | ||||
| 		*headers = append(*headers, KeyValue[string, []string]{Key: key, Value: []string{value}}) | ||||
| 	} | ||||
| } | ||||
|  | ||||
| func (headers Headers) GetValue(key string) *[]string { | ||||
| 	for i := range headers { | ||||
| 		if headers[i].Key == key { | ||||
| 			return &headers[i].Value | ||||
| 		} | ||||
| 	} | ||||
| 	return nil | ||||
| } | ||||
|  | ||||
| func (headers Headers) Has(key string) bool { | ||||
| 	for i := range headers { | ||||
| 		if headers[i].Key == key { | ||||
| 			return true | ||||
| 		} | ||||
| 	} | ||||
| 	return false | ||||
| } | ||||
|  | ||||
| func (headers *Headers) UnmarshalJSON(b []byte) error { | ||||
| 	var data []map[string]any | ||||
| 	if err := json.Unmarshal(b, &data); err != nil { | ||||
| 		return err | ||||
| 	} | ||||
|  | ||||
| 	for _, item := range data { | ||||
| 		for key, value := range item { | ||||
| 			switch parsedValue := value.(type) { | ||||
| 			case string: | ||||
| 				*headers = append(*headers, KeyValue[string, []string]{Key: key, Value: []string{parsedValue}}) | ||||
| 			case []any: | ||||
| 				parsedStr := make([]string, len(parsedValue)) | ||||
| 				for i, item := range parsedValue { | ||||
| 					parsedStr[i] = fmt.Sprintf("%v", item) | ||||
| 				} | ||||
| 				*headers = append(*headers, KeyValue[string, []string]{Key: key, Value: parsedStr}) | ||||
| 			default: | ||||
| 				return fmt.Errorf("unsupported type for headers expected string or []string, got %T", parsedValue) | ||||
| 			} | ||||
| 		} | ||||
| 	} | ||||
|  | ||||
| 	return nil | ||||
| } | ||||
|  | ||||
| func (headers *Headers) UnmarshalYAML(unmarshal func(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 | ||||
| } | ||||
| @@ -1,6 +0,0 @@ | ||||
| package types | ||||
|  | ||||
| type KeyValue[K comparable, V any] struct { | ||||
| 	Key   K | ||||
| 	Value V | ||||
| } | ||||
							
								
								
									
										139
									
								
								types/params.go
									
									
									
									
									
								
							
							
						
						
									
										139
									
								
								types/params.go
									
									
									
									
									
								
							| @@ -1,139 +0,0 @@ | ||||
| package types | ||||
|  | ||||
| import ( | ||||
| 	"bytes" | ||||
| 	"encoding/json" | ||||
| 	"fmt" | ||||
| 	"strings" | ||||
|  | ||||
| 	"github.com/jedib0t/go-pretty/v6/text" | ||||
| ) | ||||
|  | ||||
| type Params []KeyValue[string, []string] | ||||
|  | ||||
| func (params Params) String() string { | ||||
| 	var buffer bytes.Buffer | ||||
| 	if len(params) == 0 { | ||||
| 		return buffer.String() | ||||
| 	} | ||||
|  | ||||
| 	indent := "  " | ||||
|  | ||||
| 	displayLimit := 3 | ||||
|  | ||||
| 	for i, item := range params[:min(len(params), displayLimit)] { | ||||
| 		if i > 0 { | ||||
| 			buffer.WriteString(",\n") | ||||
| 		} | ||||
|  | ||||
| 		if len(item.Value) == 1 { | ||||
| 			buffer.WriteString(item.Key + ": " + item.Value[0]) | ||||
| 			continue | ||||
| 		} | ||||
| 		buffer.WriteString(item.Key + ": " + text.FgBlue.Sprint("Random") + "[\n") | ||||
|  | ||||
| 		for ii, v := range item.Value[:min(len(item.Value), displayLimit)] { | ||||
| 			if ii == len(item.Value)-1 { | ||||
| 				buffer.WriteString(indent + v + "\n") | ||||
| 			} else { | ||||
| 				buffer.WriteString(indent + v + ",\n") | ||||
| 			} | ||||
| 		} | ||||
|  | ||||
| 		// Add remaining values count if needed | ||||
| 		if remainingValues := len(item.Value) - displayLimit; remainingValues > 0 { | ||||
| 			buffer.WriteString(indent + text.FgGreen.Sprintf("+%d values", remainingValues) + "\n") | ||||
| 		} | ||||
|  | ||||
| 		buffer.WriteString("]") | ||||
| 	} | ||||
|  | ||||
| 	// Add remaining key-value pairs count if needed | ||||
| 	if remainingPairs := len(params) - displayLimit; remainingPairs > 0 { | ||||
| 		buffer.WriteString(",\n" + text.FgGreen.Sprintf("+%d params", remainingPairs)) | ||||
| 	} | ||||
|  | ||||
| 	return buffer.String() | ||||
| } | ||||
|  | ||||
| func (params *Params) AppendByKey(key, value string) { | ||||
| 	if item := params.GetValue(key); item != nil { | ||||
| 		*item = append(*item, value) | ||||
| 	} else { | ||||
| 		*params = append(*params, KeyValue[string, []string]{Key: key, Value: []string{value}}) | ||||
| 	} | ||||
| } | ||||
|  | ||||
| func (params Params) GetValue(key string) *[]string { | ||||
| 	for i := range params { | ||||
| 		if params[i].Key == key { | ||||
| 			return ¶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
									
									
									
									
									
								
							
							
						
						
									
										116
									
								
								types/proxies.go
									
									
									
									
									
								
							| @@ -1,116 +0,0 @@ | ||||
| package types | ||||
|  | ||||
| import ( | ||||
| 	"bytes" | ||||
| 	"encoding/json" | ||||
| 	"fmt" | ||||
| 	"net/url" | ||||
|  | ||||
| 	"github.com/jedib0t/go-pretty/v6/text" | ||||
| ) | ||||
|  | ||||
| type Proxies []url.URL | ||||
|  | ||||
| func (proxies Proxies) String() string { | ||||
| 	var buffer bytes.Buffer | ||||
| 	if len(proxies) == 0 { | ||||
| 		return buffer.String() | ||||
| 	} | ||||
|  | ||||
| 	if len(proxies) == 1 { | ||||
| 		buffer.WriteString(proxies[0].String()) | ||||
| 		return buffer.String() | ||||
| 	} | ||||
|  | ||||
| 	buffer.WriteString(text.FgBlue.Sprint("Random") + "[\n") | ||||
|  | ||||
| 	indent := "  " | ||||
|  | ||||
| 	displayLimit := 5 | ||||
|  | ||||
| 	for i, item := range proxies[:min(len(proxies), displayLimit)] { | ||||
| 		if i > 0 { | ||||
| 			buffer.WriteString(",\n") | ||||
| 		} | ||||
|  | ||||
| 		buffer.WriteString(indent + item.String()) | ||||
| 	} | ||||
|  | ||||
| 	// Add remaining count if there are more items | ||||
| 	if remainingValues := len(proxies) - displayLimit; remainingValues > 0 { | ||||
| 		buffer.WriteString(",\n" + indent + text.FgGreen.Sprintf("+%d proxies", remainingValues)) | ||||
| 	} | ||||
|  | ||||
| 	buffer.WriteString("\n]") | ||||
| 	return buffer.String() | ||||
| } | ||||
|  | ||||
| func (proxies *Proxies) UnmarshalJSON(b []byte) error { | ||||
| 	var data any | ||||
| 	if err := json.Unmarshal(b, &data); err != nil { | ||||
| 		return err | ||||
| 	} | ||||
|  | ||||
| 	switch v := data.(type) { | ||||
| 	case string: | ||||
| 		parsed, err := url.Parse(v) | ||||
| 		if err != nil { | ||||
| 			return err | ||||
| 		} | ||||
| 		*proxies = []url.URL{*parsed} | ||||
| 	case []any: | ||||
| 		var urls []url.URL | ||||
| 		for _, item := range v { | ||||
| 			url, err := url.Parse(item.(string)) | ||||
| 			if err != nil { | ||||
| 				return err | ||||
| 			} | ||||
| 			urls = append(urls, *url) | ||||
| 		} | ||||
| 		*proxies = urls | ||||
| 	default: | ||||
| 		return fmt.Errorf("invalid type for Body: %T (should be URL or []URL)", v) | ||||
| 	} | ||||
|  | ||||
| 	return nil | ||||
| } | ||||
|  | ||||
| func (proxies *Proxies) UnmarshalYAML(unmarshal func(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 | ||||
| } | ||||
| @@ -1,59 +0,0 @@ | ||||
| package types | ||||
|  | ||||
| import ( | ||||
| 	"encoding/json" | ||||
| 	"errors" | ||||
| 	"net/url" | ||||
| ) | ||||
|  | ||||
| type RequestURL struct { | ||||
| 	url.URL | ||||
| } | ||||
|  | ||||
| func (requestURL *RequestURL) UnmarshalJSON(data []byte) error { | ||||
| 	var urlStr string | ||||
| 	if err := json.Unmarshal(data, &urlStr); err != nil { | ||||
| 		return err | ||||
| 	} | ||||
|  | ||||
| 	parsedURL, err := url.Parse(urlStr) | ||||
| 	if err != nil { | ||||
| 		return errors.New("request URL is invalid") | ||||
| 	} | ||||
|  | ||||
| 	requestURL.URL = *parsedURL | ||||
| 	return nil | ||||
| } | ||||
|  | ||||
| func (requestURL *RequestURL) UnmarshalYAML(unmarshal func(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 | ||||
| } | ||||
| @@ -1,57 +0,0 @@ | ||||
| package types | ||||
|  | ||||
| import ( | ||||
| 	"encoding/json" | ||||
| 	"errors" | ||||
| 	"time" | ||||
| ) | ||||
|  | ||||
| type Timeout struct { | ||||
| 	time.Duration | ||||
| } | ||||
|  | ||||
| func (timeout *Timeout) UnmarshalJSON(b []byte) error { | ||||
| 	var v any | ||||
| 	if err := json.Unmarshal(b, &v); err != nil { | ||||
| 		return err | ||||
| 	} | ||||
| 	switch value := v.(type) { | ||||
| 	case float64: | ||||
| 		timeout.Duration = time.Duration(value) | ||||
| 		return nil | ||||
| 	case string: | ||||
| 		var err error | ||||
| 		timeout.Duration, err = time.ParseDuration(value) | ||||
| 		if err != nil { | ||||
| 			return errors.New("Timeout is invalid (e.g. 400ms, 1s, 5m, 1h)") | ||||
| 		} | ||||
| 		return nil | ||||
| 	default: | ||||
| 		return errors.New("Timeout is invalid (e.g. 400ms, 1s, 5m, 1h)") | ||||
| 	} | ||||
| } | ||||
|  | ||||
| func (timeout Timeout) MarshalJSON() ([]byte, error) { | ||||
| 	return json.Marshal(timeout.String()) | ||||
| } | ||||
|  | ||||
| func (timeout *Timeout) UnmarshalYAML(unmarshal func(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)") | ||||
| 	} | ||||
| } | ||||
| @@ -1,10 +0,0 @@ | ||||
| package utils | ||||
|  | ||||
| func IsNilOrZero[T comparable](value *T) bool { | ||||
| 	if value == nil { | ||||
| 		return true | ||||
| 	} | ||||
|  | ||||
| 	var zero T | ||||
| 	return *value == zero | ||||
| } | ||||
| @@ -1,5 +1,54 @@ | ||||
| package utils | ||||
|  | ||||
| func ToPtr[T any](value T) *T { | ||||
| 	return &value | ||||
| import ( | ||||
| 	"encoding/json" | ||||
| 	"fmt" | ||||
| 	"reflect" | ||||
| ) | ||||
|  | ||||
| func MarshalJSON(v any, maxSliceSize uint) string { | ||||
| 	rv := reflect.ValueOf(v) | ||||
| 	if rv.Kind() == reflect.Slice && rv.Len() == 0 { | ||||
| 		return "[]" | ||||
| 	} | ||||
|  | ||||
| 	data, err := json.MarshalIndent(truncateLists(v, int(maxSliceSize)), "", "  ") | ||||
| 	if err != nil { | ||||
| 		return "{}" | ||||
| 	} | ||||
|  | ||||
| 	return string(data) | ||||
| } | ||||
|  | ||||
| func truncateLists(v interface{}, maxItems int) interface{} { | ||||
| 	rv := reflect.ValueOf(v) | ||||
|  | ||||
| 	switch rv.Kind() { | ||||
| 	case reflect.Slice, reflect.Array: | ||||
| 		if rv.Len() > maxItems { | ||||
| 			newSlice := reflect.MakeSlice(rv.Type(), maxItems, maxItems) | ||||
| 			reflect.Copy(newSlice, rv.Slice(0, maxItems)) | ||||
| 			newSlice = reflect.Append(newSlice, reflect.ValueOf(fmt.Sprintf("...(%d more)", rv.Len()-maxItems))) | ||||
| 			return newSlice.Interface() | ||||
| 		} | ||||
| 	case reflect.Map: | ||||
| 		newMap := reflect.MakeMap(rv.Type()) | ||||
| 		for _, key := range rv.MapKeys() { | ||||
| 			newMap.SetMapIndex(key, reflect.ValueOf(truncateLists(rv.MapIndex(key).Interface(), maxItems))) | ||||
| 		} | ||||
| 		return newMap.Interface() | ||||
| 	case reflect.Struct: | ||||
| 		newStruct := reflect.New(rv.Type()).Elem() | ||||
| 		for i := 0; i < rv.NumField(); i++ { | ||||
| 			newStruct.Field(i).Set(reflect.ValueOf(truncateLists(rv.Field(i).Interface(), maxItems))) | ||||
| 		} | ||||
| 		return newStruct.Interface() | ||||
| 	case reflect.Ptr: | ||||
| 		if rv.IsNil() { | ||||
| 			return nil | ||||
| 		} | ||||
| 		return truncateLists(rv.Elem().Interface(), maxItems) | ||||
| 	} | ||||
|  | ||||
| 	return v | ||||
| } | ||||
|   | ||||
							
								
								
									
										21
									
								
								utils/int.go
									
									
									
									
									
								
							
							
						
						
									
										21
									
								
								utils/int.go
									
									
									
									
									
								
							| @@ -1,21 +0,0 @@ | ||||
| package utils | ||||
|  | ||||
| type Number interface { | ||||
| 	int | int8 | int16 | int32 | int64 | ||||
| } | ||||
|  | ||||
| func NumLen[T Number](n T) T { | ||||
| 	if n < 0 { | ||||
| 		n = -n | ||||
| 	} | ||||
| 	if n == 0 { | ||||
| 		return 1 | ||||
| 	} | ||||
|  | ||||
| 	var count T = 0 | ||||
| 	for n > 0 { | ||||
| 		n /= 10 | ||||
| 		count++ | ||||
| 	} | ||||
| 	return count | ||||
| } | ||||
| @@ -3,12 +3,46 @@ 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) { | ||||
| 	fmt.Fprintln(os.Stderr, text.FgRed.Sprint(err.Error())) | ||||
| 	PrintlnC(Colors.Red, err.Error()) | ||||
| } | ||||
|  | ||||
| func PrintErrAndExit(err error) { | ||||
|   | ||||
| @@ -2,41 +2,49 @@ 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 | ||||
| } | ||||
|  | ||||
| // 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 == valuesLen { | ||||
| 				currentIndex = 0 | ||||
| 			} | ||||
| 			if currentIndex == stopIndex { | ||||
| 				currentIndex = localRand.Intn(valuesLen) | ||||
| 				stopIndex = currentIndex | ||||
| 			} | ||||
|  | ||||
| 			return value | ||||
| 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] | ||||
| 		currentIndex++ | ||||
| 		if currentIndex == clientsCount { | ||||
| 			currentIndex = 0 | ||||
| 		} | ||||
| 		if currentIndex == stopIndex { | ||||
| 			currentIndex = localRand.Intn(clientsCount) | ||||
| 			stopIndex = currentIndex | ||||
| 		} | ||||
|  | ||||
| 		return client | ||||
| 	} | ||||
| } | ||||
|   | ||||
| @@ -1,479 +0,0 @@ | ||||
| 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, | ||||
| 	} | ||||
| } | ||||
| @@ -2,13 +2,30 @@ package utils | ||||
|  | ||||
| import "time" | ||||
|  | ||||
| func DurationRoundBy(duration time.Duration, n int64) time.Duration { | ||||
| 	if durationLen := NumLen(duration.Nanoseconds()); durationLen > n { | ||||
| 		roundNum := 1 | ||||
| 		for range durationLen - n { | ||||
| 			roundNum *= 10 | ||||
| func MinDuration(durations ...time.Duration) time.Duration { | ||||
| 	min := durations[0] | ||||
| 	for _, d := range durations { | ||||
| 		if d < min { | ||||
| 			min = d | ||||
| 		} | ||||
| 		return duration.Round(time.Duration(roundNum)) | ||||
| 	} | ||||
| 	return duration | ||||
| 	return min | ||||
| } | ||||
|  | ||||
| func MaxDuration(durations ...time.Duration) time.Duration { | ||||
| 	max := durations[0] | ||||
| 	for _, d := range durations { | ||||
| 		if d > max { | ||||
| 			max = d | ||||
| 		} | ||||
| 	} | ||||
| 	return max | ||||
| } | ||||
|  | ||||
| func AvgDuration(durations ...time.Duration) time.Duration { | ||||
| 	total := time.Duration(0) | ||||
| 	for _, d := range durations { | ||||
| 		total += d | ||||
| 	} | ||||
| 	return total / time.Duration(len(durations)) | ||||
| } | ||||
|   | ||||
							
								
								
									
										71
									
								
								utils/types.go
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										71
									
								
								utils/types.go
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,71 @@ | ||||
| package utils | ||||
|  | ||||
| import ( | ||||
| 	"encoding/json" | ||||
| 	"errors" | ||||
| ) | ||||
|  | ||||
| type NonNilConcrete interface { | ||||
| 	~int | ~float64 | ~string | ~bool | ||||
| } | ||||
|  | ||||
| type Option[T NonNilConcrete] interface { | ||||
| 	IsNone() bool | ||||
| 	ValueOrErr() (*T, error) | ||||
| 	ValueOr(def *T) *T | ||||
| 	ValueOrPanic() *T | ||||
| 	UnmarshalJSON(data []byte) error | ||||
| } | ||||
|  | ||||
| // Don't call this struct directly, use NewOption[T] or NewNoneOption[T] instead. | ||||
| type option[T NonNilConcrete] struct { | ||||
| 	// value holds the actual value of the Option if it is not None. | ||||
| 	value T | ||||
| 	// none indicates whether the Option is None (i.e., has no value). | ||||
| 	none bool | ||||
| } | ||||
|  | ||||
| func (o *option[T]) IsNone() bool { | ||||
| 	return o.none | ||||
| } | ||||
|  | ||||
| // The returned value can be nil, if the Option is None, it will return nil and an error. | ||||
| func (o *option[T]) ValueOrErr() (*T, error) { | ||||
| 	if o.IsNone() { | ||||
| 		return nil, errors.New("Option is None") | ||||
| 	} | ||||
| 	return &o.value, nil | ||||
| } | ||||
|  | ||||
| // The returned value can't be nil, if the Option is None, it will return the default value. | ||||
| func (o *option[T]) ValueOr(def *T) *T { | ||||
| 	if o.IsNone() { | ||||
| 		return def | ||||
| 	} | ||||
| 	return &o.value | ||||
| } | ||||
|  | ||||
| // The returned value can't be nil, if the Option is None, it will panic. | ||||
| func (o *option[T]) ValueOrPanic() *T { | ||||
| 	if o.IsNone() { | ||||
| 		panic("Option is None") | ||||
| 	} | ||||
| 	return &o.value | ||||
| } | ||||
|  | ||||
| func (o *option[T]) UnmarshalJSON(data []byte) error { | ||||
| 	if string(data) == "null" { | ||||
| 		o.none = true | ||||
| 		return nil | ||||
| 	} | ||||
| 	o.none = false | ||||
| 	return json.Unmarshal(data, &o.value) | ||||
| } | ||||
|  | ||||
| func NewOption[T NonNilConcrete](value T) *option[T] { | ||||
| 	return &option[T]{value: value} | ||||
| } | ||||
|  | ||||
| func NewNoneOption[T NonNilConcrete]() *option[T] { | ||||
| 	return &option[T]{none: true} | ||||
| } | ||||
							
								
								
									
										59
									
								
								validation/validator.go
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										59
									
								
								validation/validator.go
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,59 @@ | ||||
| package validation | ||||
|  | ||||
| import ( | ||||
| 	"reflect" | ||||
| 	"strings" | ||||
|  | ||||
| 	"github.com/go-playground/validator/v10" | ||||
| 	"golang.org/x/net/http/httpguts" | ||||
| ) | ||||
|  | ||||
| // net/http/request.go/isNotToken | ||||
| func isNotToken(r rune) bool { | ||||
| 	return !httpguts.IsTokenRune(r) | ||||
| } | ||||
|  | ||||
| func NewValidator() *validator.Validate { | ||||
| 	validation := validator.New() | ||||
| 	validation.RegisterTagNameFunc(func(fld reflect.StructField) string { | ||||
| 		if fld.Tag.Get("validation_name") != "" { | ||||
| 			return fld.Tag.Get("validation_name") | ||||
| 		} else { | ||||
| 			return fld.Tag.Get("json") | ||||
| 		} | ||||
| 	}) | ||||
| 	validation.RegisterValidation( | ||||
| 		"http_method", | ||||
| 		func(fl validator.FieldLevel) bool { | ||||
| 			method := fl.Field().String() | ||||
| 			// net/http/request.go/validMethod | ||||
| 			return len(method) > 0 && strings.IndexFunc(method, isNotToken) == -1 | ||||
| 		}, | ||||
| 	) | ||||
| 	validation.RegisterValidation( | ||||
| 		"string_bool", | ||||
| 		func(fl validator.FieldLevel) bool { | ||||
| 			s := fl.Field().String() | ||||
| 			return s == "true" || s == "false" || s == "" | ||||
| 		}, | ||||
| 	) | ||||
| 	validation.RegisterValidation( | ||||
| 		"proxy_url", | ||||
| 		func(fl validator.FieldLevel) bool { | ||||
| 			url := fl.Field().String() | ||||
| 			if url == "" { | ||||
| 				return false | ||||
| 			} | ||||
| 			if err := validation.Var(url, "url"); err != nil { | ||||
| 				return false | ||||
| 			} | ||||
| 			if !(url[:7] == "http://" || | ||||
| 				url[:9] == "socks5://" || | ||||
| 				url[:10] == "socks5h://") { | ||||
| 				return false | ||||
| 			} | ||||
| 			return true | ||||
| 		}, | ||||
| 	) | ||||
| 	return validation | ||||
| } | ||||
		Reference in New Issue
	
	Block a user