mirror of
				https://github.com/aykhans/dodo.git
				synced 2025-10-27 10:49:20 +00:00 
			
		
		
		
	Compare commits
	
		
			69 Commits
		
	
	
		
			v0.5.502
			...
			7e05cf4f6b
		
	
	| Author | SHA1 | Date | |
|---|---|---|---|
| 7e05cf4f6b | |||
| 934cd0ad33 | |||
| 69c4841a05 | |||
| 3cc165cbf4 | |||
| 59f40ad825 | |||
| a170588574 | |||
| 2a0ac390d8 | |||
| 11bb8b3fb0 | |||
| 1aadc3419a | |||
| b3af3f6ad5 | |||
| ed52fff363 | |||
| 985fc6200d | |||
| 1808865358 | |||
| 56342e49c6 | |||
| ec80569d5d | |||
| 459f7ee0dc | |||
| 3cd72855e5 | |||
| b8011ce651 | |||
| 0aeeb484e2 | |||
| fc3244dc33 | |||
| aa6ec450b8 | |||
| e31f5ad204 | |||
| de9a4bb355 | |||
| ![dependabot[bot]](/assets/img/avatar_default.png)  | 234ca01e41 | ||
| cc490143ea | |||
| a8c3efe198 | |||
| 3c2a0ee1b2 | |||
| 00f0bcb2de | |||
| 8f811e1bec | |||
| 58ea31683b | |||
| cc2a6eb367 | |||
| f721abb583 | |||
| ![dependabot[bot]](/assets/img/avatar_default.png)  | 4a9fb9fdda | ||
| 198b6c785a | |||
| ![dependabot[bot]](/assets/img/avatar_default.png)  | 9dc56709a7 | ||
| a9021bd1a4 | |||
| 48c2dc7935 | |||
| 4cb0540824 | |||
| a01bf19986 | |||
| 74dcecc8b1 | |||
| 00afca7139 | |||
| 1589fefeb8 | |||
| bbc43bbaac | |||
| 89fcf5f174 | |||
| a58f734a55 | |||
| efd5176ab9 | |||
| f0adcaf328 | |||
| 6314cf7724 | |||
| 140e570b85 | |||
| 83c5788e54 | |||
| ca74092615 | |||
| ![dependabot[bot]](/assets/img/avatar_default.png)  | 004b10ea3c | ||
| 4c9ceb1c4b | |||
| ![dependabot[bot]](/assets/img/avatar_default.png)  | 6f3df7c45b | ||
| 8cd495055d | |||
| ![dependabot[bot]](/assets/img/avatar_default.png)  | 391a5bc6ec | ||
| a69e248f8c | |||
| ![dependabot[bot]](/assets/img/avatar_default.png)  | 2634ca110c | ||
| b1612598c4 | |||
| ba79304b04 | |||
| 015cb15053 | |||
| 3dc002188e | |||
| 769c04685a | |||
| e43378a9a4 | |||
| e29e4f1bc6 | |||
| 7d2168a014 | |||
| ![dependabot[bot]](/assets/img/avatar_default.png)  | ba53e6b7f7 | ||
| 58964e1098 | |||
| ![dependabot[bot]](/assets/img/avatar_default.png)  | d4bf7358ff | 
| @@ -4,6 +4,7 @@ binaries | ||||
| dodo | ||||
| .git | ||||
| .gitignore | ||||
| .golangci.yml | ||||
| README.md | ||||
| LICENSE | ||||
| config.json | ||||
|   | ||||
							
								
								
									
										25
									
								
								.github/workflows/golangci-lint.yml
									
									
									
									
										vendored
									
									
										Normal file
									
								
							
							
						
						
									
										25
									
								
								.github/workflows/golangci-lint.yml
									
									
									
									
										vendored
									
									
										Normal file
									
								
							| @@ -0,0 +1,25 @@ | ||||
| name: golangci-lint | ||||
|  | ||||
| on: | ||||
|   push: | ||||
|     branches: | ||||
|       - main | ||||
|   pull_request: | ||||
|  | ||||
| permissions: | ||||
|   contents: read | ||||
|  | ||||
| jobs: | ||||
|   golangci: | ||||
|     name: lint | ||||
|     runs-on: ubuntu-latest | ||||
|     steps: | ||||
|       - uses: actions/checkout@v4 | ||||
|       - uses: actions/setup-go@v5 | ||||
|         with: | ||||
|           go-version: stable | ||||
|       - name: golangci-lint | ||||
|         uses: golangci/golangci-lint-action@v6 | ||||
|         with: | ||||
|           version: v1.64 | ||||
|           args: --timeout=10m --config=.golangci.yml | ||||
							
								
								
									
										27
									
								
								.golangci.yml
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										27
									
								
								.golangci.yml
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,27 @@ | ||||
| run: | ||||
|     go: "1.24" | ||||
|     concurrency: 8 | ||||
|     timeout: 10m | ||||
|  | ||||
| linters: | ||||
|     disable-all: true | ||||
|     enable: | ||||
|         - asasalint | ||||
|         - asciicheck | ||||
|         - errcheck | ||||
|         - gofmt | ||||
|         - goimports | ||||
|         - gomodguard | ||||
|         - goprintffuncname | ||||
|         - govet | ||||
|         - ineffassign | ||||
|         - misspell | ||||
|         - nakedret | ||||
|         - nolintlint | ||||
|         - prealloc | ||||
|         - reassign | ||||
|         - staticcheck | ||||
|         - typecheck | ||||
|         - unconvert | ||||
|         - unused | ||||
|         - whitespace | ||||
							
								
								
									
										12
									
								
								Dockerfile
									
									
									
									
									
								
							
							
						
						
									
										12
									
								
								Dockerfile
									
									
									
									
									
								
							| @@ -1,19 +1,17 @@ | ||||
| FROM golang:1.23.2-alpine AS builder | ||||
| FROM golang:1.24-alpine AS builder | ||||
|  | ||||
| WORKDIR /dodo | ||||
| WORKDIR /src | ||||
|  | ||||
| COPY go.mod go.sum ./ | ||||
| RUN go mod download | ||||
| COPY . . | ||||
|  | ||||
| RUN go build -ldflags "-s -w" -o dodo | ||||
| RUN echo "{}" > config.json | ||||
|  | ||||
| FROM gcr.io/distroless/static-debian12:latest | ||||
|  | ||||
| WORKDIR /dodo | ||||
| WORKDIR / | ||||
|  | ||||
| COPY --from=builder /dodo/dodo /dodo/dodo | ||||
| COPY --from=builder /dodo/config.json /dodo/config.json | ||||
| COPY --from=builder /src/dodo /dodo | ||||
|  | ||||
| ENTRYPOINT ["./dodo", "-c", "/dodo/config.json"] | ||||
| ENTRYPOINT ["./dodo"] | ||||
							
								
								
									
										9
									
								
								Makefile
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										9
									
								
								Makefile
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,9 @@ | ||||
| lint: | ||||
| 	golangci-lint run | ||||
|  | ||||
| build: | ||||
| 	go build -ldflags "-s -w" -o "./dodo" | ||||
|  | ||||
| build-all: | ||||
| 	rm -rf ./binaries | ||||
| 	./build.sh | ||||
							
								
								
									
										382
									
								
								README.md
									
									
									
									
									
								
							
							
						
						
									
										382
									
								
								README.md
									
									
									
									
									
								
							| @@ -1,119 +1,263 @@ | ||||
| <h1 align="center">Dodo is a simple and easy-to-use HTTP benchmarking tool.</h1> | ||||
| <p align="center"> | ||||
| <img width="30%" height="30%" src="https://ftp.aykhans.me/web/client/pubshares/hB6VSdCnBCr8gFPeiMuCji/browse?path=%2Fdodo.png"> | ||||
| </p> | ||||
|  | ||||
| ## 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 -i 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": 10, | ||||
|     "requests": 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 -i -v ./path/config.json:/dodo/config.json aykhans/dodo | ||||
| ``` | ||||
|  | ||||
| ### 3. Both (CLI & JSON) | ||||
| Override the config file arguments with CLI arguments: | ||||
|  | ||||
| ```sh | ||||
| dodo -c /path/config.json -u https://example.com -m GET -d 10 -r 1000 -t 2000 | ||||
| ``` | ||||
| With Docker: | ||||
| ```sh | ||||
| docker run --rm -i -v ./path/config.json:/dodo/config.json aykhans/dodo -u https://example.com -m GET -d 10 -r 1000 -t 2000 | ||||
| ``` | ||||
|  | ||||
| ## 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         | | ||||
| | Requests              | requests         | --requests      | -r             | Integer                          | Total number of requests to send                                    | 1000        | | ||||
| | Dodos (Threads)       | dodos            | --dodos         | -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) | -           | | ||||
| <h1 align="center">Dodo - A Fast and Easy-to-Use HTTP Benchmarking Tool</h1> | ||||
| <p align="center"> | ||||
| <img width="30%" height="30%" src="https://ftp.aykhans.me/web/client/pubshares/VzPtSHS7yPQT7ngoZzZSNU/browse?path=%2Fdodo.png"> | ||||
| </p> | ||||
|  | ||||
| ## Table of Contents | ||||
|  | ||||
| - [Installation](#installation) | ||||
|     - [Using Docker (Recommended)](#using-docker-recommended) | ||||
|     - [Using Pre-built Binaries](#using-pre-built-binaries) | ||||
|     - [Building from Source](#building-from-source) | ||||
| - [Usage](#usage) | ||||
|     - [1. CLI Usage](#1-cli-usage) | ||||
|     - [2. Config File Usage](#2-config-file-usage) | ||||
|         - [2.1 JSON Example](#21-json-example) | ||||
|         - [2.2 YAML/YML Example](#22-yamlyml-example) | ||||
|     - [3. CLI & Config File Combination](#3-cli--config-file-combination) | ||||
| - [Config Parameters Reference](#config-parameters-reference) | ||||
|  | ||||
| ## Installation | ||||
|  | ||||
| ### 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 JSON Example | ||||
|  | ||||
| ```jsonc | ||||
| { | ||||
|     "method": "GET", | ||||
|     "url": "https://example.com", | ||||
|     "yes": false, | ||||
|     "timeout": "800ms", | ||||
|     "dodos": 10, | ||||
|     "requests": 1000, | ||||
|     "duration": "250s", | ||||
|  | ||||
|     "params": [ | ||||
|         // A random value will be selected from the list for first "key1" param on each request | ||||
|         // And always "value" for second "key1" param on each request | ||||
|         // e.g. "?key1=value2&key1=value" | ||||
|         { "key1": ["value1", "value2", "value3", "value4"] }, | ||||
|         { "key1": "value" }, | ||||
|  | ||||
|         // A random value will be selected from the list for param "key2" on each request | ||||
|         // e.g. "?key2=value2" | ||||
|         { "key2": ["value1", "value2"] }, | ||||
|     ], | ||||
|  | ||||
|     "headers": [ | ||||
|         // A random value will be selected from the list for first "key1" header on each request | ||||
|         // And always "value" for second "key1" header on each request | ||||
|         // e.g. "key1: value3", "key1: value" | ||||
|         { "key1": ["value1", "value2", "value3", "value4"] }, | ||||
|         { "key1": "value" }, | ||||
|  | ||||
|         // A random value will be selected from the list for header "key2" on each request | ||||
|         // e.g. "key2: value2" | ||||
|         { "key2": ["value1", "value2"] }, | ||||
|     ], | ||||
|  | ||||
|     "cookies": [ | ||||
|         // A random value will be selected from the list for first "key1" cookie on each request | ||||
|         // And always "value" for second "key1" cookie on each request | ||||
|         // e.g. "key1=value4; key1=value" | ||||
|         { "key1": ["value1", "value2", "value3", "value4"] }, | ||||
|         { "key1": "value" }, | ||||
|  | ||||
|         // A random value will be selected from the list for cookie "key2" on each request | ||||
|         // e.g. "key2=value1" | ||||
|         { "key2": ["value1", "value2"] }, | ||||
|     ], | ||||
|  | ||||
|     "body": "body-text", | ||||
|     // OR | ||||
|     // A random body value will be selected from the list for each request | ||||
|     "body": ["body-text1", "body-text2", "body-text3"], | ||||
|  | ||||
|     "proxy": "http://example.com:8080", | ||||
|     // OR | ||||
|     // A random proxy will be selected from the list for each request | ||||
|     "proxy": [ | ||||
|         "http://example.com:8080", | ||||
|         "http://username:password@example.com:8080", | ||||
|         "socks5://example.com:8080", | ||||
|         "socks5h://example.com:8080", | ||||
|     ], | ||||
| } | ||||
| ``` | ||||
|  | ||||
| ```sh | ||||
| dodo -f /path/config.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 | ||||
| ``` | ||||
|  | ||||
| #### 2.2 YAML/YML Example | ||||
|  | ||||
| ```yaml | ||||
| method: "GET" | ||||
| url: "https://example.com" | ||||
| yes: false | ||||
| timeout: "800ms" | ||||
| dodos: 10 | ||||
| requests: 1000 | ||||
| duration: "250s" | ||||
|  | ||||
| params: | ||||
|     # A random value will be selected from the list for first "key1" param on each request | ||||
|     # And always "value" for second "key1" param on each request | ||||
|     # e.g. "?key1=value2&key1=value" | ||||
|     - key1: ["value1", "value2", "value3", "value4"] | ||||
|     - key1: "value" | ||||
|  | ||||
|     # A random value will be selected from the list for param "key2" on each request | ||||
|     # e.g. "?key2=value2" | ||||
|     - key2: ["value1", "value2"] | ||||
|  | ||||
| headers: | ||||
|     # A random value will be selected from the list for first "key1" header on each request | ||||
|     # And always "value" for second "key1" header on each request | ||||
|     # e.g. "key1: value3", "key1: value" | ||||
|     - key1: ["value1", "value2", "value3", "value4"] | ||||
|     - key1: "value" | ||||
|  | ||||
|     # A random value will be selected from the list for header "key2" on each request | ||||
|     # e.g. "key2: value2" | ||||
|     - key2: ["value1", "value2"] | ||||
|  | ||||
| cookies: | ||||
|     # A random value will be selected from the list for first "key1" cookie on each request | ||||
|     # And always "value" for second "key1" cookie on each request | ||||
|     # e.g. "key1=value4; key1=value" | ||||
|     - key1: ["value1", "value2", "value3", "value4"] | ||||
|     - key1: "value" | ||||
|  | ||||
|     # A random value will be selected from the list for cookie "key2" on each request | ||||
|     # e.g. "key2=value1" | ||||
|     - key2: ["value1", "value2"] | ||||
|  | ||||
| body: "body-text" | ||||
| # OR | ||||
| # A random body value will be selected from the list for each request | ||||
| body: | ||||
|     - "body-text1" | ||||
|     - "body-text2" | ||||
|     - "body-text3" | ||||
|  | ||||
| proxy: "http://example.com:8080" | ||||
| # OR | ||||
| # A random proxy will be selected from the list for each request | ||||
| proxy: | ||||
|     - "http://example.com:8080" | ||||
|     - "http://username:password@example.com:8080" | ||||
|     - "socks5://example.com:8080" | ||||
|     - "socks5h://example.com:8080" | ||||
| ``` | ||||
|  | ||||
| ```sh | ||||
| dodo -f /path/config.yaml | ||||
| # OR | ||||
| dodo -f https://example.com/config.yaml | ||||
| ``` | ||||
|  | ||||
| With Docker: | ||||
|  | ||||
| ```sh | ||||
| docker run --rm -i -v /path/to/config.yaml:/config.yaml aykhans/dodo -f /config.yaml | ||||
| # OR | ||||
| docker run --rm -i aykhans/dodo -f https://example.com/config.yaml | ||||
| ``` | ||||
|  | ||||
| ### 3. CLI & Config File Combination | ||||
|  | ||||
| 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 | ||||
| ``` | ||||
|  | ||||
| ## 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                             | -       | | ||||
|   | ||||
							
								
								
									
										50
									
								
								config.json
									
									
									
									
									
								
							
							
						
						
									
										50
									
								
								config.json
									
									
									
									
									
								
							| @@ -1,22 +1,36 @@ | ||||
| { | ||||
|     "method": "GET", | ||||
|     "url": "https://example.com", | ||||
|     "no_proxy_check": false, | ||||
|     "timeout": 10000, | ||||
|     "dodos": 1, | ||||
|     "requests": 1, | ||||
|     "params": {}, | ||||
|     "headers": {}, | ||||
|     "cookies": {}, | ||||
|     "body": [], | ||||
|     "proxies": [ | ||||
|         { | ||||
|             "url": "http://example.com:8080", | ||||
|             "username": "username", | ||||
|             "password": "password" | ||||
|         }, | ||||
|         { | ||||
|             "url": "http://example.com:8080" | ||||
|         } | ||||
|     "yes": false, | ||||
|     "timeout": "5s", | ||||
|     "dodos": 8, | ||||
|     "requests": 1000, | ||||
|     "duration": "10s", | ||||
|  | ||||
|     "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" | ||||
|     ] | ||||
| } | ||||
| } | ||||
							
								
								
									
										39
									
								
								config.yaml
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										39
									
								
								config.yaml
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,39 @@ | ||||
| method: "GET" | ||||
| url: "https://example.com" | ||||
| yes: false | ||||
| timeout: "5s" | ||||
| dodos: 8 | ||||
| requests: 1000 | ||||
| duration: "10s" | ||||
|  | ||||
| params: | ||||
|     - key1: ["value1", "value2", "value3", "value4"] | ||||
|     - key1: "value" | ||||
|     - key2: ["value1", "value2"] | ||||
|  | ||||
| headers: | ||||
|     - key1: ["value1", "value2", "value3", "value4"] | ||||
|     - key1: "value" | ||||
|     - key2: ["value1", "value2"] | ||||
|  | ||||
| cookies: | ||||
|     - key1: ["value1", "value2", "value3", "value4"] | ||||
|     - key1: "value" | ||||
|     - key2: ["value1", "value2"] | ||||
|  | ||||
| # body: "body-text" | ||||
| # OR | ||||
| # A random body value will be selected from the list for each request | ||||
| body: | ||||
|     - "body-text1" | ||||
|     - "body-text2" | ||||
|     - "body-text3" | ||||
|  | ||||
| # proxy: "http://example.com:8080" | ||||
| # OR | ||||
| # A random proxy will be selected from the list for each request | ||||
| proxy: | ||||
|     - "http://example.com:8080" | ||||
|     - "http://username:password@example.com:8080" | ||||
|     - "socks5://example.com:8080" | ||||
|     - "socks5h://example.com:8080" | ||||
							
								
								
									
										181
									
								
								config/cli.go
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										181
									
								
								config/cli.go
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,181 @@ | ||||
| package config | ||||
|  | ||||
| import ( | ||||
| 	"flag" | ||||
| 	"fmt" | ||||
| 	"os" | ||||
| 	"strings" | ||||
| 	"time" | ||||
|  | ||||
| 	"github.com/aykhans/dodo/types" | ||||
| 	"github.com/aykhans/dodo/utils" | ||||
| ) | ||||
|  | ||||
| const cliUsageText = `Usage: | ||||
|   dodo [flags] | ||||
|  | ||||
| Examples: | ||||
|  | ||||
| Simple usage: | ||||
|   dodo -u https://example.com -o 1m | ||||
|  | ||||
| Usage with config file: | ||||
|   dodo -f /path/to/config/file/config.json | ||||
|  | ||||
| Usage with all flags: | ||||
|   dodo -f /path/to/config/file/config.json \ | ||||
|     -u https://example.com -m POST \ | ||||
|     -d 10 -r 1000 -o 3m -t 3s \ | ||||
|     -b "body1" -body "body2" \ | ||||
|     -H "header1:value1" -header "header2:value2" \ | ||||
|     -p "param1=value1" -param "param2=value2" \ | ||||
|     -c "cookie1=value1" -cookie "cookie2=value2" \ | ||||
|     -x "http://proxy.example.com:8080" -proxy "socks5://proxy2.example.com:8080" \ | ||||
|     -y | ||||
|  | ||||
| Flags: | ||||
|   -h, -help                   help for dodo | ||||
|   -v, -version                version for dodo | ||||
|   -y, -yes          bool      Answer yes to all questions (default %v) | ||||
|   -f, -config-file  string    Path to the local config file or http(s) URL of the config file | ||||
|   -d, -dodos        uint      Number of dodos(threads) (default %d) | ||||
|   -r, -requests     uint      Number of total requests | ||||
|   -o, -duration     Time      Maximum duration for the test (e.g. 30s, 1m, 5h) | ||||
|   -t, -timeout      Time      Timeout for each request (e.g. 400ms, 15s, 1m10s) (default %v) | ||||
|   -u, -url          string    URL for stress testing | ||||
|   -m, -method       string    HTTP Method for the request (default %s) | ||||
|   -b, -body         [string]  Body for the request (e.g. "body text") | ||||
|   -p, -param        [string]  Parameter for the request (e.g. "key1=value1") | ||||
|   -H, -header       [string]  Header for the request (e.g. "key1:value1") | ||||
|   -c, -cookie       [string]  Cookie for the request (e.g. "key1=value1") | ||||
|   -x, -proxy        [string]  Proxy for the request (e.g. "http://proxy.example.com:8080")` | ||||
|  | ||||
| func (config *Config) ReadCLI() (types.ConfigFile, error) { | ||||
| 	flag.Usage = func() { | ||||
| 		fmt.Printf( | ||||
| 			cliUsageText+"\n", | ||||
| 			DefaultYes, | ||||
| 			DefaultDodosCount, | ||||
| 			DefaultTimeout, | ||||
| 			DefaultMethod, | ||||
| 		) | ||||
| 	} | ||||
|  | ||||
| 	var ( | ||||
| 		version      = false | ||||
| 		configFile   = "" | ||||
| 		yes          = false | ||||
| 		method       = "" | ||||
| 		url          types.RequestURL | ||||
| 		dodosCount   = uint(0) | ||||
| 		requestCount = uint(0) | ||||
| 		timeout      time.Duration | ||||
| 		duration     time.Duration | ||||
| 	) | ||||
|  | ||||
| 	{ | ||||
| 		flag.BoolVar(&version, "version", false, "Prints the version of the program") | ||||
| 		flag.BoolVar(&version, "v", false, "Prints the version of the program") | ||||
|  | ||||
| 		flag.StringVar(&configFile, "config-file", "", "Path to the configuration file") | ||||
| 		flag.StringVar(&configFile, "f", "", "Path to the configuration file") | ||||
|  | ||||
| 		flag.BoolVar(&yes, "yes", false, "Answer yes to all questions") | ||||
| 		flag.BoolVar(&yes, "y", false, "Answer yes to all questions") | ||||
|  | ||||
| 		flag.StringVar(&method, "method", "", "HTTP Method") | ||||
| 		flag.StringVar(&method, "m", "", "HTTP Method") | ||||
|  | ||||
| 		flag.Var(&url, "url", "URL to send the request") | ||||
| 		flag.Var(&url, "u", "URL to send the request") | ||||
|  | ||||
| 		flag.UintVar(&dodosCount, "dodos", 0, "Number of dodos(threads)") | ||||
| 		flag.UintVar(&dodosCount, "d", 0, "Number of dodos(threads)") | ||||
|  | ||||
| 		flag.UintVar(&requestCount, "requests", 0, "Number of total requests") | ||||
| 		flag.UintVar(&requestCount, "r", 0, "Number of total requests") | ||||
|  | ||||
| 		flag.DurationVar(&duration, "duration", 0, "Maximum duration of the test") | ||||
| 		flag.DurationVar(&duration, "o", 0, "Maximum duration of the test") | ||||
|  | ||||
| 		flag.DurationVar(&timeout, "timeout", 0, "Timeout for each request (e.g. 400ms, 15s, 1m10s)") | ||||
| 		flag.DurationVar(&timeout, "t", 0, "Timeout for each request (e.g. 400ms, 15s, 1m10s)") | ||||
|  | ||||
| 		flag.Var(&config.Params, "param", "URL parameter to send with the request") | ||||
| 		flag.Var(&config.Params, "p", "URL parameter to send with the request") | ||||
|  | ||||
| 		flag.Var(&config.Headers, "header", "Header to send with the request") | ||||
| 		flag.Var(&config.Headers, "H", "Header to send with the request") | ||||
|  | ||||
| 		flag.Var(&config.Cookies, "cookie", "Cookie to send with the request") | ||||
| 		flag.Var(&config.Cookies, "c", "Cookie to send with the request") | ||||
|  | ||||
| 		flag.Var(&config.Body, "body", "Body to send with the request") | ||||
| 		flag.Var(&config.Body, "b", "Body to send with the request") | ||||
|  | ||||
| 		flag.Var(&config.Proxies, "proxy", "Proxy to use for the request") | ||||
| 		flag.Var(&config.Proxies, "x", "Proxy to use for the request") | ||||
| 	} | ||||
|  | ||||
| 	flag.Parse() | ||||
|  | ||||
| 	if len(os.Args) <= 1 { | ||||
| 		flag.CommandLine.Usage() | ||||
| 		os.Exit(0) | ||||
| 	} | ||||
|  | ||||
| 	if args := flag.Args(); len(args) > 0 { | ||||
| 		return types.ConfigFile(configFile), fmt.Errorf("unexpected arguments: %v", strings.Join(args, ", ")) | ||||
| 	} | ||||
|  | ||||
| 	if version { | ||||
| 		fmt.Printf("dodo version %s\n", VERSION) | ||||
| 		os.Exit(0) | ||||
| 	} | ||||
|  | ||||
| 	flag.Visit(func(f *flag.Flag) { | ||||
| 		switch f.Name { | ||||
| 		case "method", "m": | ||||
| 			config.Method = utils.ToPtr(method) | ||||
| 		case "url", "u": | ||||
| 			config.URL = utils.ToPtr(url) | ||||
| 		case "dodos", "d": | ||||
| 			config.DodosCount = utils.ToPtr(dodosCount) | ||||
| 		case "requests", "r": | ||||
| 			config.RequestCount = utils.ToPtr(requestCount) | ||||
| 		case "duration", "o": | ||||
| 			config.Duration = &types.Duration{Duration: duration} | ||||
| 		case "timeout", "t": | ||||
| 			config.Timeout = &types.Timeout{Duration: timeout} | ||||
| 		case "yes", "y": | ||||
| 			config.Yes = utils.ToPtr(yes) | ||||
| 		} | ||||
| 	}) | ||||
|  | ||||
| 	return types.ConfigFile(configFile), nil | ||||
| } | ||||
|  | ||||
| // CLIYesOrNoReader reads a yes or no answer from the command line. | ||||
| // It prompts the user with the given message and default value, | ||||
| // and returns true if the user answers "y" or "Y", and false otherwise. | ||||
| // If there is an error while reading the input, it returns false. | ||||
| // If the user simply presses enter without providing any input, | ||||
| // it returns the default value specified by the `dft` parameter. | ||||
| func CLIYesOrNoReader(message string, dft bool) bool { | ||||
| 	var answer string | ||||
| 	defaultMessage := "Y/n" | ||||
| 	if !dft { | ||||
| 		defaultMessage = "y/N" | ||||
| 	} | ||||
| 	fmt.Printf("%s [%s]: ", message, defaultMessage) | ||||
| 	if _, err := fmt.Scanln(&answer); err != nil { | ||||
| 		if err.Error() == "unexpected newline" { | ||||
| 			return dft | ||||
| 		} | ||||
| 		return false | ||||
| 	} | ||||
| 	if answer == "" { | ||||
| 		return dft | ||||
| 	} | ||||
| 	return answer == "y" || answer == "Y" | ||||
| } | ||||
							
								
								
									
										317
									
								
								config/config.go
									
									
									
									
									
								
							
							
						
						
									
										317
									
								
								config/config.go
									
									
									
									
									
								
							| @@ -1,43 +1,79 @@ | ||||
| package config | ||||
|  | ||||
| import ( | ||||
| 	"errors" | ||||
| 	"fmt" | ||||
| 	"net/url" | ||||
| 	"os" | ||||
| 	"slices" | ||||
| 	"strings" | ||||
| 	"time" | ||||
|  | ||||
| 	. "github.com/aykhans/dodo/types" | ||||
| 	"github.com/aykhans/dodo/types" | ||||
| 	"github.com/aykhans/dodo/utils" | ||||
| 	"github.com/jedib0t/go-pretty/v6/table" | ||||
| ) | ||||
|  | ||||
| const ( | ||||
| 	VERSION                 string = "0.5.502" | ||||
| 	DefaultUserAgent        string = "Dodo/" + VERSION | ||||
| 	ProxyCheckURL           string = "https://www.google.com" | ||||
| 	DefaultMethod           string = "GET" | ||||
| 	DefaultTimeout          uint32 = 10000 // Milliseconds (10 seconds) | ||||
| 	DefaultDodosCount       uint   = 1 | ||||
| 	DefaultRequestCount     uint   = 1 | ||||
| 	MaxDodosCountForProxies uint   = 20 // Max dodos count for proxy check | ||||
| 	VERSION             string        = "0.6.2" | ||||
| 	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 | ||||
| ) | ||||
|  | ||||
| var SupportedProxySchemes []string = []string{"http", "socks5", "socks5h"} | ||||
|  | ||||
| type RequestConfig struct { | ||||
| 	Method       string | ||||
| 	URL          *url.URL | ||||
| 	URL          url.URL | ||||
| 	Timeout      time.Duration | ||||
| 	DodosCount   uint | ||||
| 	RequestCount uint | ||||
| 	Params       map[string][]string | ||||
| 	Headers      map[string][]string | ||||
| 	Cookies      map[string][]string | ||||
| 	Proxies      []Proxy | ||||
| 	Body         []string | ||||
| 	Duration     time.Duration | ||||
| 	Yes          bool | ||||
| 	NoProxyCheck bool | ||||
| 	Params       types.Params | ||||
| 	Headers      types.Headers | ||||
| 	Cookies      types.Cookies | ||||
| 	Body         types.Body | ||||
| 	Proxies      types.Proxies | ||||
| } | ||||
|  | ||||
| func (config *RequestConfig) Print() { | ||||
| func NewRequestConfig(conf *Config) *RequestConfig { | ||||
| 	return &RequestConfig{ | ||||
| 		Method:       *conf.Method, | ||||
| 		URL:          conf.URL.URL, | ||||
| 		Timeout:      conf.Timeout.Duration, | ||||
| 		DodosCount:   *conf.DodosCount, | ||||
| 		RequestCount: *conf.RequestCount, | ||||
| 		Duration:     conf.Duration.Duration, | ||||
| 		Yes:          *conf.Yes, | ||||
| 		Params:       conf.Params, | ||||
| 		Headers:      conf.Headers, | ||||
| 		Cookies:      conf.Cookies, | ||||
| 		Body:         conf.Body, | ||||
| 		Proxies:      conf.Proxies, | ||||
| 	} | ||||
| } | ||||
|  | ||||
| func (rc *RequestConfig) GetValidDodosCountForRequests() uint { | ||||
| 	if rc.RequestCount == 0 { | ||||
| 		return rc.DodosCount | ||||
| 	} | ||||
| 	return min(rc.DodosCount, rc.RequestCount) | ||||
| } | ||||
|  | ||||
| func (rc *RequestConfig) GetMaxConns(minConns uint) uint { | ||||
| 	maxConns := max( | ||||
| 		minConns, rc.GetValidDodosCountForRequests(), | ||||
| 	) | ||||
| 	return ((maxConns * 50 / 100) + maxConns) | ||||
| } | ||||
|  | ||||
| func (rc *RequestConfig) Print() { | ||||
| 	t := table.NewWriter() | ||||
| 	t.SetOutputMirror(os.Stdout) | ||||
| 	t.SetStyle(table.StyleLight) | ||||
| @@ -56,151 +92,134 @@ func (config *RequestConfig) Print() { | ||||
| 			WidthMax: 50}, | ||||
| 	}) | ||||
|  | ||||
| 	newHeaders := make(map[string][]string) | ||||
| 	newHeaders["User-Agent"] = []string{DefaultUserAgent} | ||||
| 	for k, v := range config.Headers { | ||||
| 		newHeaders[k] = v | ||||
| 	} | ||||
|  | ||||
| 	t.AppendHeader(table.Row{"Request Configuration"}) | ||||
| 	t.AppendRow(table.Row{"Method", config.Method}) | ||||
| 	t.AppendRow(table.Row{"URL", rc.URL.String()}) | ||||
| 	t.AppendSeparator() | ||||
| 	t.AppendRow(table.Row{"URL", config.URL}) | ||||
| 	t.AppendRow(table.Row{"Method", rc.Method}) | ||||
| 	t.AppendSeparator() | ||||
| 	t.AppendRow(table.Row{"Timeout", config.Timeout}) | ||||
| 	t.AppendRow(table.Row{"Timeout", rc.Timeout}) | ||||
| 	t.AppendSeparator() | ||||
| 	t.AppendRow(table.Row{"Dodos", config.DodosCount}) | ||||
| 	t.AppendRow(table.Row{"Dodos", rc.DodosCount}) | ||||
| 	t.AppendSeparator() | ||||
| 	t.AppendRow(table.Row{"Requests", config.RequestCount}) | ||||
| 	if rc.RequestCount > 0 { | ||||
| 		t.AppendRow(table.Row{"Requests", rc.RequestCount}) | ||||
| 	} else { | ||||
| 		t.AppendRow(table.Row{"Requests"}) | ||||
| 	} | ||||
| 	t.AppendSeparator() | ||||
| 	t.AppendRow(table.Row{"Params", string(utils.PrettyJSONMarshal(config.Params, 3, "", "  "))}) | ||||
| 	if rc.Duration > 0 { | ||||
| 		t.AppendRow(table.Row{"Duration", rc.Duration}) | ||||
| 	} else { | ||||
| 		t.AppendRow(table.Row{"Duration"}) | ||||
| 	} | ||||
| 	t.AppendSeparator() | ||||
| 	t.AppendRow(table.Row{"Headers", string(utils.PrettyJSONMarshal(newHeaders, 3, "", "  "))}) | ||||
| 	t.AppendRow(table.Row{"Params", rc.Params.String()}) | ||||
| 	t.AppendSeparator() | ||||
| 	t.AppendRow(table.Row{"Cookies", string(utils.PrettyJSONMarshal(config.Cookies, 3, "", "  "))}) | ||||
| 	t.AppendRow(table.Row{"Headers", rc.Headers.String()}) | ||||
| 	t.AppendSeparator() | ||||
| 	t.AppendRow(table.Row{"Proxies", string(utils.PrettyJSONMarshal(config.Proxies, 3, "", "  "))}) | ||||
| 	t.AppendRow(table.Row{"Cookies", rc.Cookies.String()}) | ||||
| 	t.AppendSeparator() | ||||
| 	t.AppendRow(table.Row{"Proxy Check", !config.NoProxyCheck}) | ||||
| 	t.AppendRow(table.Row{"Proxy", rc.Proxies.String()}) | ||||
| 	t.AppendSeparator() | ||||
| 	t.AppendRow(table.Row{"Body", string(utils.PrettyJSONMarshal(config.Body, 3, "", "  "))}) | ||||
| 	t.AppendRow(table.Row{"Body", rc.Body.String()}) | ||||
|  | ||||
| 	t.Render() | ||||
| } | ||||
|  | ||||
| func (config *RequestConfig) GetValidDodosCountForRequests() uint { | ||||
| 	return min(config.DodosCount, config.RequestCount) | ||||
| } | ||||
|  | ||||
| func (config *RequestConfig) GetValidDodosCountForProxies() uint { | ||||
| 	return min(config.DodosCount, uint(len(config.Proxies)), MaxDodosCountForProxies) | ||||
| } | ||||
|  | ||||
| func (config *RequestConfig) GetMaxConns(minConns uint) uint { | ||||
| 	maxConns := max( | ||||
| 		minConns, uint(config.GetValidDodosCountForRequests()), | ||||
| 	) | ||||
| 	return ((maxConns * 50 / 100) + maxConns) | ||||
| } | ||||
|  | ||||
| type Config struct { | ||||
| 	Method       string       `json:"method" validate:"http_method"` // custom validations: http_method | ||||
| 	URL          string       `json:"url" validate:"http_url,required"` | ||||
| 	Timeout      uint32       `json:"timeout" validate:"gte=1,lte=100000"` | ||||
| 	DodosCount   uint         `json:"dodos" validate:"gte=1"` | ||||
| 	RequestCount uint         `json:"requests" validation_name:"request-count" validate:"gte=1"` | ||||
| 	NoProxyCheck Option[bool] `json:"no_proxy_check"` | ||||
| 	Method       *string           `json:"method" yaml:"method"` | ||||
| 	URL          *types.RequestURL `json:"url" yaml:"url"` | ||||
| 	Timeout      *types.Timeout    `json:"timeout" yaml:"timeout"` | ||||
| 	DodosCount   *uint             `json:"dodos" yaml:"dodos"` | ||||
| 	RequestCount *uint             `json:"requests" yaml:"requests"` | ||||
| 	Duration     *types.Duration   `json:"duration" yaml:"duration"` | ||||
| 	Yes          *bool             `json:"yes" yaml:"yes"` | ||||
| 	Params       types.Params      `json:"params" yaml:"params"` | ||||
| 	Headers      types.Headers     `json:"headers" yaml:"headers"` | ||||
| 	Cookies      types.Cookies     `json:"cookies" yaml:"cookies"` | ||||
| 	Body         types.Body        `json:"body" yaml:"body"` | ||||
| 	Proxies      types.Proxies     `json:"proxy" yaml:"proxy"` | ||||
| } | ||||
|  | ||||
| func NewConfig( | ||||
| 	method string, | ||||
| 	timeout uint32, | ||||
| 	dodosCount uint, | ||||
| 	requestCount uint, | ||||
| 	noProxyCheck Option[bool], | ||||
| ) *Config { | ||||
| 	if noProxyCheck == nil { | ||||
| 		noProxyCheck = NewNoneOption[bool]() | ||||
| 	} | ||||
|  | ||||
| 	return &Config{ | ||||
| 		Method:       method, | ||||
| 		Timeout:      timeout, | ||||
| 		DodosCount:   dodosCount, | ||||
| 		RequestCount: requestCount, | ||||
| 		NoProxyCheck: noProxyCheck, | ||||
| 	} | ||||
| func NewConfig() *Config { | ||||
| 	return &Config{} | ||||
| } | ||||
|  | ||||
| func (config *Config) MergeConfigs(newConfig *Config) { | ||||
| 	if newConfig.Method != "" { | ||||
| func (c *Config) Validate() []error { | ||||
| 	var errs []error | ||||
| 	if utils.IsNilOrZero(c.URL) { | ||||
| 		errs = append(errs, errors.New("request URL is required")) | ||||
| 	} else { | ||||
| 		if c.URL.Scheme == "" { | ||||
| 			c.URL.Scheme = "http" | ||||
| 		} | ||||
| 		if c.URL.Scheme != "http" && c.URL.Scheme != "https" { | ||||
| 			errs = append(errs, errors.New("request URL scheme must be http or https")) | ||||
| 		} | ||||
|  | ||||
| 		urlParams := types.Params{} | ||||
| 		for key, values := range c.URL.Query() { | ||||
| 			for _, value := range values { | ||||
| 				urlParams = append(urlParams, types.KeyValue[string, []string]{ | ||||
| 					Key:   key, | ||||
| 					Value: []string{value}, | ||||
| 				}) | ||||
| 			} | ||||
| 		} | ||||
| 		c.Params = append(urlParams, c.Params...) | ||||
| 		c.URL.RawQuery = "" | ||||
| 	} | ||||
|  | ||||
| 	if utils.IsNilOrZero(c.Method) { | ||||
| 		errs = append(errs, errors.New("request method is required")) | ||||
| 	} | ||||
| 	if utils.IsNilOrZero(c.Timeout) { | ||||
| 		errs = append(errs, errors.New("request timeout must be greater than 0")) | ||||
| 	} | ||||
| 	if utils.IsNilOrZero(c.DodosCount) { | ||||
| 		errs = append(errs, errors.New("dodos count must be greater than 0")) | ||||
| 	} | ||||
| 	if utils.IsNilOrZero(c.Duration) && utils.IsNilOrZero(c.RequestCount) { | ||||
| 		errs = append(errs, errors.New("you should provide at least one of duration or request count")) | ||||
| 	} | ||||
|  | ||||
| 	for i, proxy := range c.Proxies { | ||||
| 		if proxy.String() == "" { | ||||
| 			errs = append(errs, fmt.Errorf("proxies[%d]: proxy cannot be empty", i)) | ||||
| 		} else if schema := proxy.Scheme; !slices.Contains(SupportedProxySchemes, schema) { | ||||
| 			errs = append(errs, | ||||
| 				fmt.Errorf("proxies[%d]: proxy has unsupported scheme \"%s\" (supported schemes: %s)", | ||||
| 					i, proxy.String(), strings.Join(SupportedProxySchemes, ", "), | ||||
| 				), | ||||
| 			) | ||||
| 		} | ||||
| 	} | ||||
|  | ||||
| 	return errs | ||||
| } | ||||
|  | ||||
| func (config *Config) MergeConfig(newConfig *Config) { | ||||
| 	if newConfig.Method != nil { | ||||
| 		config.Method = newConfig.Method | ||||
| 	} | ||||
| 	if newConfig.URL != "" { | ||||
| 	if newConfig.URL != nil { | ||||
| 		config.URL = newConfig.URL | ||||
| 	} | ||||
| 	if newConfig.Timeout != 0 { | ||||
| 	if newConfig.Timeout != nil { | ||||
| 		config.Timeout = newConfig.Timeout | ||||
| 	} | ||||
| 	if newConfig.DodosCount != 0 { | ||||
| 	if newConfig.DodosCount != nil { | ||||
| 		config.DodosCount = newConfig.DodosCount | ||||
| 	} | ||||
| 	if newConfig.RequestCount != 0 { | ||||
| 	if newConfig.RequestCount != nil { | ||||
| 		config.RequestCount = newConfig.RequestCount | ||||
| 	} | ||||
| 	if !newConfig.NoProxyCheck.IsNone() { | ||||
| 		config.NoProxyCheck = newConfig.NoProxyCheck | ||||
| 	if newConfig.Duration != nil { | ||||
| 		config.Duration = newConfig.Duration | ||||
| 	} | ||||
| } | ||||
|  | ||||
| func (config *Config) SetDefaults() { | ||||
| 	if config.Method == "" { | ||||
| 		config.Method = DefaultMethod | ||||
| 	if newConfig.Yes != nil { | ||||
| 		config.Yes = newConfig.Yes | ||||
| 	} | ||||
| 	if config.Timeout == 0 { | ||||
| 		config.Timeout = DefaultTimeout | ||||
| 	} | ||||
| 	if config.DodosCount == 0 { | ||||
| 		config.DodosCount = DefaultDodosCount | ||||
| 	} | ||||
| 	if config.RequestCount == 0 { | ||||
| 		config.RequestCount = DefaultRequestCount | ||||
| 	} | ||||
| 	if config.NoProxyCheck.IsNone() { | ||||
| 		config.NoProxyCheck = NewOption(false) | ||||
| 	} | ||||
| } | ||||
|  | ||||
| type Proxy struct { | ||||
| 	URL      string `json:"url" validate:"required,proxy_url"` | ||||
| 	Username string `json:"username"` | ||||
| 	Password string `json:"password"` | ||||
| } | ||||
|  | ||||
| type JSONConfig struct { | ||||
| 	*Config | ||||
| 	Params  map[string][]string `json:"params"` | ||||
| 	Headers map[string][]string `json:"headers"` | ||||
| 	Cookies map[string][]string `json:"cookies"` | ||||
| 	Proxies []Proxy             `json:"proxies" validate:"dive"` | ||||
| 	Body    []string            `json:"body"` | ||||
| } | ||||
|  | ||||
| func NewJSONConfig( | ||||
| 	config *Config, | ||||
| 	params map[string][]string, | ||||
| 	headers map[string][]string, | ||||
| 	cookies map[string][]string, | ||||
| 	proxies []Proxy, | ||||
| 	body []string, | ||||
| ) *JSONConfig { | ||||
| 	return &JSONConfig{ | ||||
| 		config, params, headers, cookies, proxies, body, | ||||
| 	} | ||||
| } | ||||
|  | ||||
| func (config *JSONConfig) MergeConfigs(newConfig *JSONConfig) { | ||||
| 	config.Config.MergeConfigs(newConfig.Config) | ||||
| 	if len(newConfig.Params) != 0 { | ||||
| 		config.Params = newConfig.Params | ||||
| 	} | ||||
| @@ -218,28 +237,24 @@ func (config *JSONConfig) MergeConfigs(newConfig *JSONConfig) { | ||||
| 	} | ||||
| } | ||||
|  | ||||
| type CLIConfig struct { | ||||
| 	*Config | ||||
| 	Yes        Option[bool] `json:"yes" validate:"omitempty"` | ||||
| 	ConfigFile string       `validation_name:"config-file" validate:"omitempty,filepath"` | ||||
| } | ||||
|  | ||||
| func NewCLIConfig( | ||||
| 	config *Config, | ||||
| 	yes Option[bool], | ||||
| 	configFile string, | ||||
| ) *CLIConfig { | ||||
| 	return &CLIConfig{ | ||||
| 		config, yes, configFile, | ||||
| func (config *Config) SetDefaults() { | ||||
| 	if config.Method == nil { | ||||
| 		config.Method = utils.ToPtr(DefaultMethod) | ||||
| 	} | ||||
| } | ||||
|  | ||||
| func (config *CLIConfig) MergeConfigs(newConfig *CLIConfig) { | ||||
| 	config.Config.MergeConfigs(newConfig.Config) | ||||
| 	if newConfig.ConfigFile != "" { | ||||
| 		config.ConfigFile = newConfig.ConfigFile | ||||
| 	if config.Timeout == nil { | ||||
| 		config.Timeout = &types.Timeout{Duration: DefaultTimeout} | ||||
| 	} | ||||
| 	if !newConfig.Yes.IsNone() { | ||||
| 		config.Yes = newConfig.Yes | ||||
| 	if config.DodosCount == nil { | ||||
| 		config.DodosCount = utils.ToPtr(DefaultDodosCount) | ||||
| 	} | ||||
| 	if config.RequestCount == nil { | ||||
| 		config.RequestCount = utils.ToPtr(DefaultRequestCount) | ||||
| 	} | ||||
| 	if config.Duration == nil { | ||||
| 		config.Duration = &types.Duration{Duration: DefaultDuration} | ||||
| 	} | ||||
| 	if config.Yes == nil { | ||||
| 		config.Yes = utils.ToPtr(DefaultYes) | ||||
| 	} | ||||
| 	config.Headers.SetIfNotExists("User-Agent", DefaultUserAgent) | ||||
| } | ||||
|   | ||||
							
								
								
									
										83
									
								
								config/file.go
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										83
									
								
								config/file.go
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,83 @@ | ||||
| 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 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()) | ||||
| 			} | ||||
| 		} | ||||
|  | ||||
| 		if fileExt == "json" { | ||||
| 			return parseJSONConfig(data, config) | ||||
| 		} else if fileExt == "yml" || fileExt == "yaml" { | ||||
| 			return parseYAMLConfig(data, config) | ||||
| 		} | ||||
| 	} | ||||
|  | ||||
| 	return fmt.Errorf("unsupported config file type (supported types: %v)", strings.Join(supportedFileTypes, ", ")) | ||||
| } | ||||
|  | ||||
| func parseJSONConfig(data []byte, config *Config) error { | ||||
| 	err := json.Unmarshal(data, &config) | ||||
| 	if err != nil { | ||||
| 		switch parsedErr := err.(type) { | ||||
| 		case *json.SyntaxError: | ||||
| 			return fmt.Errorf("JSON Config file: invalid syntax at byte offset %d", parsedErr.Offset) | ||||
| 		case *json.UnmarshalTypeError: | ||||
| 			return fmt.Errorf("JSON Config file: invalid type %v for field %s, expected %v", parsedErr.Value, parsedErr.Field, parsedErr.Type) | ||||
| 		default: | ||||
| 			return fmt.Errorf("JSON Config file: %s", err.Error()) | ||||
| 		} | ||||
| 	} | ||||
|  | ||||
| 	return nil | ||||
| } | ||||
|  | ||||
| func parseYAMLConfig(data []byte, config *Config) error { | ||||
| 	err := yaml.Unmarshal(data, &config) | ||||
| 	if err != nil { | ||||
| 		return fmt.Errorf("YAML Config file: %s", err.Error()) | ||||
| 	} | ||||
|  | ||||
| 	return nil | ||||
| } | ||||
| @@ -1,117 +0,0 @@ | ||||
| package customerrors | ||||
|  | ||||
| import ( | ||||
| 	"errors" | ||||
| 	"fmt" | ||||
|  | ||||
| 	"github.com/go-playground/validator/v10" | ||||
| ) | ||||
|  | ||||
| var ( | ||||
| 	ErrInvalidJSON = errors.New("invalid JSON file") | ||||
| 	ErrInvalidFile = errors.New("invalid file") | ||||
| 	ErrInterrupt   = errors.New("interrupted") | ||||
| 	ErrNoInternet  = errors.New("no internet connection") | ||||
| 	ErrTimeout     = errors.New("timeout") | ||||
| ) | ||||
|  | ||||
| func As(err error, target any) bool { | ||||
| 	return errors.As(err, target) | ||||
| } | ||||
|  | ||||
| func Is(err, target error) bool { | ||||
| 	return errors.Is(err, target) | ||||
| } | ||||
|  | ||||
| type Error interface { | ||||
| 	Error() string | ||||
| 	Unwrap() error | ||||
| } | ||||
|  | ||||
| type TypeError struct { | ||||
| 	Expected string | ||||
| 	Received string | ||||
| 	Field    string | ||||
| 	err      error | ||||
| } | ||||
|  | ||||
| func NewTypeError(expected, received, field string, err error) *TypeError { | ||||
| 	return &TypeError{ | ||||
| 		Expected: expected, | ||||
| 		Received: received, | ||||
| 		Field:    field, | ||||
| 		err:      err, | ||||
| 	} | ||||
| } | ||||
|  | ||||
| func (e *TypeError) Error() string { | ||||
| 	return "Expected " + e.Expected + " but received " + e.Received + " in field " + e.Field | ||||
| } | ||||
|  | ||||
| func (e *TypeError) Unwrap() error { | ||||
| 	return e.err | ||||
| } | ||||
|  | ||||
| type InvalidFileError struct { | ||||
| 	FileName string | ||||
| 	err      error | ||||
| } | ||||
|  | ||||
| func NewInvalidFileError(fileName string, err error) *InvalidFileError { | ||||
| 	return &InvalidFileError{ | ||||
| 		FileName: fileName, | ||||
| 		err:      err, | ||||
| 	} | ||||
| } | ||||
|  | ||||
| func (e *InvalidFileError) Error() string { | ||||
| 	return "Invalid file: " + e.FileName | ||||
| } | ||||
|  | ||||
| func (e *InvalidFileError) Unwrap() error { | ||||
| 	return e.err | ||||
| } | ||||
|  | ||||
| type FileNotFoundError struct { | ||||
| 	FileName string | ||||
| 	err      error | ||||
| } | ||||
|  | ||||
| func NewFileNotFoundError(fileName string, err error) *FileNotFoundError { | ||||
| 	return &FileNotFoundError{ | ||||
| 		FileName: fileName, | ||||
| 		err:      err, | ||||
| 	} | ||||
| } | ||||
|  | ||||
| func (e *FileNotFoundError) Error() string { | ||||
| 	return "File not found: " + e.FileName | ||||
| } | ||||
|  | ||||
| func (e *FileNotFoundError) Unwrap() error { | ||||
| 	return e.err | ||||
| } | ||||
|  | ||||
| type ValidationErrors struct { | ||||
| 	MapErrors map[string]string | ||||
| 	errors    validator.ValidationErrors | ||||
| } | ||||
|  | ||||
| func NewValidationErrors(errsMap map[string]string, errs validator.ValidationErrors) *ValidationErrors { | ||||
| 	return &ValidationErrors{ | ||||
| 		MapErrors: errsMap, | ||||
| 		errors:    errs, | ||||
| 	} | ||||
| } | ||||
|  | ||||
| func (errs *ValidationErrors) Error() string { | ||||
| 	var errorsStr string | ||||
| 	for k, v := range errs.MapErrors { | ||||
| 		errorsStr += fmt.Sprintf("[%s]: %s\n", k, v) | ||||
| 	} | ||||
| 	return errorsStr | ||||
| } | ||||
|  | ||||
| func (errs *ValidationErrors) Unwrap() error { | ||||
| 	return errs.errors | ||||
| } | ||||
| @@ -1,68 +0,0 @@ | ||||
| package customerrors | ||||
|  | ||||
| import ( | ||||
| 	"fmt" | ||||
| 	"net" | ||||
| 	"net/url" | ||||
| 	"strings" | ||||
|  | ||||
| 	"github.com/go-playground/validator/v10" | ||||
| ) | ||||
|  | ||||
| func OSErrorFormater(err error) error { | ||||
| 	errStr := err.Error() | ||||
| 	if strings.Contains(errStr, "no such file or directory") { | ||||
| 		fileName1 := strings.Index(errStr, "open") | ||||
| 		fileName2 := strings.LastIndex(errStr, ":") | ||||
| 		return NewFileNotFoundError(errStr[fileName1+5:fileName2], err) | ||||
| 	} | ||||
| 	return ErrInvalidFile | ||||
| } | ||||
|  | ||||
| func shortenNamespace(namespace string) string { | ||||
| 	return namespace[strings.Index(namespace, ".")+1:] | ||||
| } | ||||
|  | ||||
| func ValidationErrorsFormater(errs validator.ValidationErrors) error { | ||||
| 	errsStr := make(map[string]string) | ||||
| 	for _, err := range errs { | ||||
| 		switch err.Tag() { | ||||
| 		case "required": | ||||
| 			errsStr[shortenNamespace(err.Namespace())] = fmt.Sprintf("Field \"%s\" is required", err.Field()) | ||||
| 		case "gte": | ||||
| 			errsStr[shortenNamespace(err.Namespace())] = fmt.Sprintf("Value of \"%s\" must be greater than or equal to \"%s\"", err.Field(), err.Param()) | ||||
| 		case "lte": | ||||
| 			errsStr[shortenNamespace(err.Namespace())] = fmt.Sprintf("Value of \"%s\" must be less than or equal to \"%s\"", err.Field(), err.Param()) | ||||
| 		case "filepath": | ||||
| 			errsStr[shortenNamespace(err.Namespace())] = fmt.Sprintf("Invalid file path for \"%s\" field: \"%s\"", err.Field(), err.Value()) | ||||
| 		case "http_url": | ||||
| 			errsStr[shortenNamespace(err.Namespace())] = | ||||
| 				fmt.Sprintf("Invalid url for \"%s\" field: \"%s\"", err.Field(), err.Value()) | ||||
| 		// --------------------------------------| Custom validations |-------------------------------------- | ||||
| 		case "http_method": | ||||
| 			errsStr[shortenNamespace(err.Namespace())] = fmt.Sprintf("Invalid HTTP method for \"%s\" field: \"%s\"", err.Field(), err.Value()) | ||||
| 		case "proxy_url": | ||||
| 			errsStr[shortenNamespace(err.Namespace())] = fmt.Sprintf("Invalid proxy url for \"%s\" field: \"%s\" (it must be http, socks5 or socks5h)", err.Field(), err.Value()) | ||||
| 		case "string_bool": | ||||
| 			errsStr[shortenNamespace(err.Namespace())] = fmt.Sprintf("Invalid value for \"%s\" field: \"%s\"", err.Field(), err.Value()) | ||||
| 		default: | ||||
| 			errsStr[shortenNamespace(err.Namespace())] = fmt.Sprintf("Invalid value for \"%s\" field: \"%s\"", err.Field(), err.Value()) | ||||
| 		} | ||||
| 	} | ||||
| 	return NewValidationErrors(errsStr, errs) | ||||
| } | ||||
|  | ||||
| func RequestErrorsFormater(err error) string { | ||||
| 	switch e := err.(type) { | ||||
| 	case *url.Error: | ||||
| 		if netErr, ok := e.Err.(net.Error); ok && netErr.Timeout() { | ||||
| 			return "Timeout Error" | ||||
| 		} | ||||
| 		if strings.Contains(e.Error(), "http: ContentLength=") { | ||||
| 			println(e.Error()) | ||||
| 			return "Empty Body Error" | ||||
| 		} | ||||
| 		// TODO: Add more cases | ||||
| 	} | ||||
| 	return "Unknown Error" | ||||
| } | ||||
							
								
								
									
										23
									
								
								go.mod
									
									
									
									
									
								
							
							
						
						
									
										23
									
								
								go.mod
									
									
									
									
									
								
							| @@ -1,26 +1,21 @@ | ||||
| module github.com/aykhans/dodo | ||||
|  | ||||
| go 1.23.2 | ||||
| go 1.24.0 | ||||
|  | ||||
| require ( | ||||
| 	github.com/go-playground/validator/v10 v10.23.0 | ||||
| 	github.com/jedib0t/go-pretty/v6 v6.6.5 | ||||
| 	github.com/valyala/fasthttp v1.58.0 | ||||
| 	golang.org/x/net v0.33.0 | ||||
| 	github.com/jedib0t/go-pretty/v6 v6.6.7 | ||||
| 	github.com/valyala/fasthttp v1.59.0 | ||||
| 	gopkg.in/yaml.v3 v3.0.1 | ||||
| ) | ||||
|  | ||||
| require ( | ||||
| 	github.com/andybalholm/brotli v1.1.1 // indirect | ||||
| 	github.com/gabriel-vasile/mimetype v1.4.4 // indirect | ||||
| 	github.com/go-playground/locales v0.14.1 // indirect | ||||
| 	github.com/go-playground/universal-translator v0.18.1 // indirect | ||||
| 	github.com/klauspost/compress v1.17.11 // indirect | ||||
| 	github.com/leodido/go-urn v1.4.0 // indirect | ||||
| 	github.com/mattn/go-runewidth v0.0.15 // indirect | ||||
| 	github.com/mattn/go-runewidth v0.0.16 // indirect | ||||
| 	github.com/rivo/uniseg v0.4.7 // indirect | ||||
| 	github.com/valyala/bytebufferpool v1.0.0 // indirect | ||||
| 	golang.org/x/crypto v0.31.0 // indirect | ||||
| 	golang.org/x/sys v0.28.0 // indirect | ||||
| 	golang.org/x/term v0.27.0 // indirect | ||||
| 	golang.org/x/text v0.21.0 // indirect | ||||
| 	golang.org/x/net v0.36.0 // indirect | ||||
| 	golang.org/x/sys v0.30.0 // indirect | ||||
| 	golang.org/x/term v0.29.0 // indirect | ||||
| 	golang.org/x/text v0.22.0 // indirect | ||||
| ) | ||||
|   | ||||
							
								
								
									
										48
									
								
								go.sum
									
									
									
									
									
								
							
							
						
						
									
										48
									
								
								go.sum
									
									
									
									
									
								
							| @@ -2,46 +2,34 @@ github.com/andybalholm/brotli v1.1.1 h1:PR2pgnyFznKEugtsUo0xLdDop5SKXd5Qf5ysW+7X | ||||
| github.com/andybalholm/brotli v1.1.1/go.mod h1:05ib4cKhjx3OQYUY22hTVd34Bc8upXjOLL2rKwwZBoA= | ||||
| github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c= | ||||
| github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= | ||||
| github.com/gabriel-vasile/mimetype v1.4.4 h1:QjV6pZ7/XZ7ryI2KuyeEDE8wnh7fHP9YnQy+R0LnH8I= | ||||
| github.com/gabriel-vasile/mimetype v1.4.4/go.mod h1:JwLei5XPtWdGiMFB5Pjle1oEeoSeEuJfJE+TtfvdB/s= | ||||
| github.com/go-playground/assert/v2 v2.2.0 h1:JvknZsQTYeFEAhQwI4qEt9cyV5ONwRHC+lYKSsYSR8s= | ||||
| github.com/go-playground/assert/v2 v2.2.0/go.mod h1:VDjEfimB/XKnb+ZQfWdccd7VUvScMdVu0Titje2rxJ4= | ||||
| github.com/go-playground/locales v0.14.1 h1:EWaQ/wswjilfKLTECiXz7Rh+3BjFhfDFKv/oXslEjJA= | ||||
| github.com/go-playground/locales v0.14.1/go.mod h1:hxrqLVvrK65+Rwrd5Fc6F2O76J/NuW9t0sjnWqG1slY= | ||||
| github.com/go-playground/universal-translator v0.18.1 h1:Bcnm0ZwsGyWbCzImXv+pAJnYK9S473LQFuzCbDbfSFY= | ||||
| github.com/go-playground/universal-translator v0.18.1/go.mod h1:xekY+UJKNuX9WP91TpwSH2VMlDf28Uj24BCp08ZFTUY= | ||||
| github.com/go-playground/validator/v10 v10.23.0 h1:/PwmTwZhS0dPkav3cdK9kV1FsAmrL8sThn8IHr/sO+o= | ||||
| github.com/go-playground/validator/v10 v10.23.0/go.mod h1:dbuPbCMFw/DrkbEynArYaCwl3amGuJotoKCe95atGMM= | ||||
| github.com/jedib0t/go-pretty/v6 v6.6.5 h1:9PgMJOVBedpgYLI56jQRJYqngxYAAzfEUua+3NgSqAo= | ||||
| github.com/jedib0t/go-pretty/v6 v6.6.5/go.mod h1:Uq/HrbhuFty5WSVNfjpQQe47x16RwVGXIveNGEyGtHs= | ||||
| github.com/jedib0t/go-pretty/v6 v6.6.7 h1:m+LbHpm0aIAPLzLbMfn8dc3Ht8MW7lsSO4MPItz/Uuo= | ||||
| github.com/jedib0t/go-pretty/v6 v6.6.7/go.mod h1:YwC5CE4fJ1HFUDeivSV1r//AmANFHyqczZk+U6BDALU= | ||||
| github.com/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/mattn/go-runewidth v0.0.16 h1:E5ScNMtiwvlvB5paMFdw9p4kSQzbXFikJ5SQO6TULQc= | ||||
| github.com/mattn/go-runewidth v0.0.16/go.mod h1:Jdepj2loyihRzMpdS35Xk/zdY8IAYHsh153qUoGf23w= | ||||
| github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM= | ||||
| github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= | ||||
| github.com/rivo/uniseg v0.2.0/go.mod h1:J6wj4VEh+S6ZtnVlnTBMWIodfgj8LQOQFoIToxlJtxc= | ||||
| github.com/rivo/uniseg v0.4.7 h1:WUdvkW8uEhrYfLC4ZzdpI2ztxP1I582+49Oc5Mq64VQ= | ||||
| github.com/rivo/uniseg v0.4.7/go.mod h1:FN3SvrM+Zdj16jyLfmOkMNblXMcoc8DfTHruCPUcx88= | ||||
| github.com/stretchr/testify v1.8.4 h1:CcVxjf3Q8PM0mHUKJCdn+eZZtm5yQwehR5yeSVQQcUk= | ||||
| github.com/stretchr/testify v1.8.4/go.mod h1:sz/lmYIOXD/1dqDmKjjqLyZ2RngseejIcXlSw2iwfAo= | ||||
| github.com/stretchr/testify v1.10.0 h1:Xv5erBjTwe/5IxqUQTdXv5kgmIvbHo3QQyRwhJsOfJA= | ||||
| github.com/stretchr/testify v1.10.0/go.mod h1:r2ic/lqez/lEtzL7wO/rwa5dbSLXVDPFyf8C91i36aY= | ||||
| github.com/valyala/bytebufferpool v1.0.0 h1:GqA5TC/0021Y/b9FG4Oi9Mr3q7XYx6KllzawFIhcdPw= | ||||
| github.com/valyala/bytebufferpool v1.0.0/go.mod h1:6bBcMArwyJ5K/AmCkWv1jt77kVWyCJ6HpOuEn7z0Csc= | ||||
| github.com/valyala/fasthttp v1.58.0 h1:GGB2dWxSbEprU9j0iMJHgdKYJVDyjrOwF9RE59PbRuE= | ||||
| github.com/valyala/fasthttp v1.58.0/go.mod h1:SYXvHHaFp7QZHGKSHmoMipInhrI5StHrhDTYVEjK/Kw= | ||||
| github.com/valyala/fasthttp v1.59.0 h1:Qu0qYHfXvPk1mSLNqcFtEk6DpxgA26hy6bmydotDpRI= | ||||
| github.com/valyala/fasthttp v1.59.0/go.mod h1:GTxNb9Bc6r2a9D0TWNSPwDz78UxnTGBViY3xZNEqyYU= | ||||
| github.com/xyproto/randomstring v1.0.5 h1:YtlWPoRdgMu3NZtP45drfy1GKoojuR7hmRcnhZqKjWU= | ||||
| github.com/xyproto/randomstring v1.0.5/go.mod h1:rgmS5DeNXLivK7YprL0pY+lTuhNQW3iGxZ18UQApw/E= | ||||
| golang.org/x/crypto v0.31.0 h1:ihbySMvVjLAeSH1IbfcRTkD/iNscyz8rGzjF/E5hV6U= | ||||
| golang.org/x/crypto v0.31.0/go.mod h1:kDsLvtWBEx7MV9tJOj9bnXsPbxwJQ6csT/x4KIN4Ssk= | ||||
| golang.org/x/net v0.33.0 h1:74SYHlV8BIgHIFC/LrYkOGIwL19eTYXQ5wc6TBuO36I= | ||||
| golang.org/x/net v0.33.0/go.mod h1:HXLR5J+9DxmrqMwG9qjGCxZ+zKXxBru04zlTvWlWuN4= | ||||
| golang.org/x/sys v0.28.0 h1:Fksou7UEQUWlKvIdsqzJmUmCX3cZuD2+P3XyyzwMhlA= | ||||
| golang.org/x/sys v0.28.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA= | ||||
| golang.org/x/term v0.27.0 h1:WP60Sv1nlK1T6SupCHbXzSaN0b9wUmsPoRS9b61A23Q= | ||||
| golang.org/x/term v0.27.0/go.mod h1:iMsnZpn0cago0GOrHO2+Y7u7JPn5AylBrcoWkElMTSM= | ||||
| golang.org/x/text v0.21.0 h1:zyQAAkrwaneQ066sspRyJaG9VNi/YJ1NfzcGB3hZ/qo= | ||||
| golang.org/x/text v0.21.0/go.mod h1:4IBbMaMmOPCJ8SecivzSH54+73PCFmPWxNTLm+vZkEQ= | ||||
| golang.org/x/net v0.36.0 h1:vWF2fRbw4qslQsQzgFqZff+BItCvGFQqKzKIzx1rmoA= | ||||
| golang.org/x/net v0.36.0/go.mod h1:bFmbeoIPfrw4sMHNhb4J9f6+tPziuGjq7Jk/38fxi1I= | ||||
| golang.org/x/sys v0.30.0 h1:QjkSwP/36a20jFYWkSue1YwXzLmsV5Gfq7Eiy72C1uc= | ||||
| golang.org/x/sys v0.30.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA= | ||||
| golang.org/x/term v0.29.0 h1:L6pJp37ocefwRRtYPKSWOWzOtWSxVajvz2ldH/xi3iU= | ||||
| golang.org/x/term v0.29.0/go.mod h1:6bl4lRlvVuDgSf3179VpIxBF0o10JUpXWOnI7nErv7s= | ||||
| golang.org/x/text v0.22.0 h1:bofq7m3/HAFvbF51jz3Q9wLg3jkvSPuiZu/pD1XwgtM= | ||||
| golang.org/x/text v0.22.0/go.mod h1:YRoo4H8PVmsu+E3Ou7cqLVH8oXWIHVoX0jqUWALQhfY= | ||||
| gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405 h1:yhCVgyC4o1eVCa2tZl7eS0r+SDo693bJlVdllGtEeKM= | ||||
| gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= | ||||
| gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA= | ||||
| gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= | ||||
|   | ||||
							
								
								
									
										132
									
								
								main.go
									
									
									
									
									
								
							
							
						
						
									
										132
									
								
								main.go
									
									
									
									
									
								
							| @@ -2,121 +2,73 @@ package main | ||||
|  | ||||
| import ( | ||||
| 	"context" | ||||
| 	"errors" | ||||
| 	"fmt" | ||||
| 	"net/url" | ||||
| 	"os" | ||||
| 	"os/signal" | ||||
| 	"strings" | ||||
| 	"syscall" | ||||
| 	"time" | ||||
|  | ||||
| 	"github.com/aykhans/dodo/config" | ||||
| 	customerrors "github.com/aykhans/dodo/custom_errors" | ||||
| 	"github.com/aykhans/dodo/readers" | ||||
| 	"github.com/aykhans/dodo/requests" | ||||
| 	"github.com/aykhans/dodo/types" | ||||
| 	"github.com/aykhans/dodo/utils" | ||||
| 	"github.com/aykhans/dodo/validation" | ||||
| 	goValidator "github.com/go-playground/validator/v10" | ||||
| 	"github.com/jedib0t/go-pretty/v6/text" | ||||
| ) | ||||
|  | ||||
| func main() { | ||||
| 	validator := validation.NewValidator() | ||||
| 	conf := config.NewConfig("", 0, 0, 0, nil) | ||||
| 	jsonConf := config.NewJSONConfig( | ||||
| 		config.NewConfig("", 0, 0, 0, nil), nil, nil, nil, nil, nil, | ||||
| 	) | ||||
|  | ||||
| 	cliConf, err := readers.CLIConfigReader() | ||||
| 	if err != nil { | ||||
| 		utils.PrintAndExit(err.Error()) | ||||
| 	} | ||||
| 	if cliConf == nil { | ||||
| 		os.Exit(0) | ||||
| 	} | ||||
|  | ||||
| 	if err := validator.StructPartial(cliConf, "ConfigFile"); err != nil { | ||||
| 		utils.PrintErrAndExit( | ||||
| 			customerrors.ValidationErrorsFormater( | ||||
| 				err.(goValidator.ValidationErrors), | ||||
| 			), | ||||
| 		) | ||||
| 	} | ||||
| 	if cliConf.ConfigFile != "" { | ||||
| 		jsonConfNew, err := readers.JSONConfigReader(cliConf.ConfigFile) | ||||
| 		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) | ||||
| 	conf := config.NewConfig() | ||||
| 	configFile, err := conf.ReadCLI() | ||||
| 	if err != nil { | ||||
| 		utils.PrintErrAndExit(err) | ||||
| 	} | ||||
| 	requestConf := &config.RequestConfig{ | ||||
| 		Method:       conf.Method, | ||||
| 		URL:          parsedURL, | ||||
| 		Timeout:      time.Duration(conf.Timeout) * time.Millisecond, | ||||
| 		DodosCount:   conf.DodosCount, | ||||
| 		RequestCount: conf.RequestCount, | ||||
| 		Params:       jsonConf.Params, | ||||
| 		Headers:      jsonConf.Headers, | ||||
| 		Cookies:      jsonConf.Cookies, | ||||
| 		Proxies:      jsonConf.Proxies, | ||||
| 		Body:         jsonConf.Body, | ||||
| 		Yes:          cliConf.Yes.ValueOr(false), | ||||
| 		NoProxyCheck: conf.NoProxyCheck.ValueOr(false), | ||||
| 	} | ||||
| 	requestConf.Print() | ||||
| 	if !cliConf.Yes.ValueOr(false) { | ||||
| 		response := readers.CLIYesOrNoReader("Do you want to continue?", true) | ||||
| 		if !response { | ||||
| 			utils.PrintAndExit("Exiting...") | ||||
|  | ||||
| 	if configFile.String() != "" { | ||||
| 		tempConf := config.NewConfig() | ||||
| 		if err := tempConf.ReadFile(configFile); err != nil { | ||||
| 			utils.PrintErrAndExit(err) | ||||
| 		} | ||||
| 		tempConf.MergeConfig(conf) | ||||
| 		conf = tempConf | ||||
| 	} | ||||
| 	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 !response { | ||||
| 			utils.PrintAndExit("Exiting...\n") | ||||
| 		} | ||||
| 		fmt.Println() | ||||
| 	} | ||||
|  | ||||
| 	ctx, cancel := context.WithCancel(context.Background()) | ||||
| 	sigChan := make(chan os.Signal, 1) | ||||
| 	signal.Notify(sigChan, syscall.SIGINT, syscall.SIGTERM) | ||||
| 	go func() { | ||||
| 		<-sigChan | ||||
| 		cancel() | ||||
| 	}() | ||||
| 	go listenForTermination(func() { cancel() }) | ||||
|  | ||||
| 	if requestConf.Duration > 0 { | ||||
| 		time.AfterFunc(requestConf.Duration, func() { cancel() }) | ||||
| 	} | ||||
|  | ||||
| 	responses, err := requests.Run(ctx, requestConf) | ||||
| 	if err != nil { | ||||
| 		if customerrors.Is(err, customerrors.ErrInterrupt) { | ||||
| 			utils.PrintlnC(utils.Colors.Yellow, err.Error()) | ||||
| 			return | ||||
| 		} else if customerrors.Is(err, customerrors.ErrNoInternet) { | ||||
| 			utils.PrintAndExit("No internet connection") | ||||
| 		if err == types.ErrInterrupt { | ||||
| 			fmt.Println(text.FgYellow.Sprint(err.Error())) | ||||
| 			return | ||||
| 		} | ||||
| 		panic(err) | ||||
| 		utils.PrintErrAndExit(err) | ||||
| 	} | ||||
|  | ||||
| 	responses.Print() | ||||
| } | ||||
|  | ||||
| func listenForTermination(do func()) { | ||||
| 	sigChan := make(chan os.Signal, 1) | ||||
| 	signal.Notify(sigChan, syscall.SIGINT, syscall.SIGTERM) | ||||
| 	<-sigChan | ||||
| 	do() | ||||
| } | ||||
|   | ||||
							
								
								
									
										155
									
								
								readers/cli.go
									
									
									
									
									
								
							
							
						
						
									
										155
									
								
								readers/cli.go
									
									
									
									
									
								
							| @@ -1,155 +0,0 @@ | ||||
| package readers | ||||
|  | ||||
| import ( | ||||
| 	"flag" | ||||
| 	"fmt" | ||||
| 	"strings" | ||||
|  | ||||
| 	"github.com/aykhans/dodo/config" | ||||
| 	. "github.com/aykhans/dodo/types" | ||||
| 	"github.com/aykhans/dodo/utils" | ||||
| ) | ||||
|  | ||||
| const usageText = `Usage: | ||||
|   dodo [flags] | ||||
|  | ||||
| Examples: | ||||
|  | ||||
| Simple usage only with URL: | ||||
|   dodo -u https://example.com | ||||
|  | ||||
| Simple usage with config file: | ||||
|   dodo -c /path/to/config/file/config.json | ||||
|  | ||||
| Usage with all flags: | ||||
|   dodo -c /path/to/config/file/config.json -u https://example.com -m POST -d 10 -r 1000 -t 2000 --no-proxy-check -y | ||||
|  | ||||
| Flags: | ||||
|   -h, --help                 help for dodo | ||||
|   -v, --version              version for dodo | ||||
|   -c, --config-file string   Path to the config file | ||||
|   -d, --dodos uint           Number of dodos(threads) (default %d) | ||||
|   -m, --method string        HTTP Method (default %s) | ||||
|   -r, --request uint         Number of total requests (default %d) | ||||
|   -t, --timeout uint32       Timeout for each request in milliseconds (default %d) | ||||
|   -u, --url string           URL for stress testing | ||||
|       --no-proxy-check bool  Do not check for proxies (default false) | ||||
|   -y, --yes bool             Answer yes to all questions (default false)` | ||||
|  | ||||
| func CLIConfigReader() (*config.CLIConfig, error) { | ||||
| 	flag.Usage = func() { | ||||
| 		fmt.Printf( | ||||
| 			usageText+"\n", | ||||
| 			config.DefaultDodosCount, | ||||
| 			config.DefaultMethod, | ||||
| 			config.DefaultRequestCount, | ||||
| 			config.DefaultTimeout, | ||||
| 		) | ||||
| 	} | ||||
|  | ||||
| 	var ( | ||||
| 		cliConfig          = config.NewCLIConfig(config.NewConfig("", 0, 0, 0, nil), NewOption(false), "") | ||||
| 		configFile         = "" | ||||
| 		yes                = false | ||||
| 		method             = "" | ||||
| 		url                = "" | ||||
| 		dodosCount    uint = 0 | ||||
| 		requestsCount uint = 0 | ||||
| 		timeout       uint = 0 | ||||
| 		noProxyCheck  bool = false | ||||
| 	) | ||||
| 	{ | ||||
| 		flag.Bool("version", false, "Prints the version of the program") | ||||
| 		flag.Bool("v", false, "Prints the version of the program") | ||||
|  | ||||
| 		flag.StringVar(&configFile, "config-file", "", "Path to the configuration file") | ||||
| 		flag.StringVar(&configFile, "c", "", "Path to the configuration file") | ||||
|  | ||||
| 		flag.BoolVar(&yes, "yes", false, "Answer yes to all questions") | ||||
| 		flag.BoolVar(&yes, "y", false, "Answer yes to all questions") | ||||
|  | ||||
| 		flag.StringVar(&method, "method", "", "HTTP Method") | ||||
| 		flag.StringVar(&method, "m", "", "HTTP Method") | ||||
|  | ||||
| 		flag.StringVar(&url, "url", "", "URL to send the request") | ||||
| 		flag.StringVar(&url, "u", "", "URL to send the request") | ||||
|  | ||||
| 		flag.UintVar(&dodosCount, "dodos", 0, "Number of dodos(threads)") | ||||
| 		flag.UintVar(&dodosCount, "d", 0, "Number of dodos(threads)") | ||||
|  | ||||
| 		flag.UintVar(&requestsCount, "requests", 0, "Number of total requests") | ||||
| 		flag.UintVar(&requestsCount, "r", 0, "Number of total requests") | ||||
|  | ||||
| 		flag.UintVar(&timeout, "timeout", 0, "Timeout for each request in milliseconds") | ||||
| 		flag.UintVar(&timeout, "t", 0, "Timeout for each request in milliseconds") | ||||
|  | ||||
| 		flag.BoolVar(&noProxyCheck, "no-proxy-check", false, "Do not check for active proxies") | ||||
| 	} | ||||
|  | ||||
| 	flag.Parse() | ||||
|  | ||||
| 	args := flag.Args() | ||||
| 	if len(args) > 0 { | ||||
| 		return nil, fmt.Errorf("unexpected arguments: %v", strings.Join(args, ", ")) | ||||
| 	} | ||||
|  | ||||
| 	returnNil := false | ||||
| 	flag.Visit(func(f *flag.Flag) { | ||||
| 		switch f.Name { | ||||
| 		case "version", "v": | ||||
| 			fmt.Printf("dodo version %s\n", config.VERSION) | ||||
| 			returnNil = true | ||||
| 		case "config-file", "c": | ||||
| 			cliConfig.ConfigFile = configFile | ||||
| 		case "yes", "y": | ||||
| 			cliConfig.Yes.SetValue(yes) | ||||
| 		case "method", "m": | ||||
| 			cliConfig.Method = method | ||||
| 		case "url", "u": | ||||
| 			cliConfig.URL = url | ||||
| 		case "dodos", "d": | ||||
| 			cliConfig.DodosCount = dodosCount | ||||
| 		case "requests", "r": | ||||
| 			cliConfig.RequestCount = requestsCount | ||||
| 		case "timeout", "t": | ||||
| 			var maxUint32 uint = 4294967295 | ||||
| 			if timeout > maxUint32 { | ||||
| 				utils.PrintfC(utils.Colors.Yellow, "timeout value is too large, setting to %d\n", maxUint32) | ||||
| 				timeout = maxUint32 | ||||
| 			} | ||||
| 			cliConfig.Timeout = uint32(timeout) | ||||
| 		case "no-proxy-check": | ||||
| 			cliConfig.NoProxyCheck.SetValue(noProxyCheck) | ||||
| 		} | ||||
| 	}) | ||||
|  | ||||
| 	if returnNil { | ||||
| 		return nil, nil | ||||
| 	} | ||||
| 	return cliConfig, nil | ||||
| } | ||||
|  | ||||
| // CLIYesOrNoReader reads a yes or no answer from the command line. | ||||
| // It prompts the user with the given message and default value, | ||||
| // and returns true if the user answers "y" or "Y", and false otherwise. | ||||
| // If there is an error while reading the input, it returns false. | ||||
| // If the user simply presses enter without providing any input, | ||||
| // it returns the default value specified by the `dft` parameter. | ||||
| func CLIYesOrNoReader(message string, dft bool) bool { | ||||
| 	var answer string | ||||
| 	defaultMessage := "Y/n" | ||||
| 	if !dft { | ||||
| 		defaultMessage = "y/N" | ||||
| 	} | ||||
| 	fmt.Printf("%s [%s]: ", message, defaultMessage) | ||||
| 	if _, err := fmt.Scanln(&answer); err != nil { | ||||
| 		if err.Error() == "unexpected newline" { | ||||
| 			return dft | ||||
| 		} | ||||
| 		return false | ||||
| 	} | ||||
| 	if answer == "" { | ||||
| 		return dft | ||||
| 	} | ||||
| 	return answer == "y" || answer == "Y" | ||||
| } | ||||
| @@ -1,36 +0,0 @@ | ||||
| package readers | ||||
|  | ||||
| import ( | ||||
| 	"encoding/json" | ||||
| 	"os" | ||||
|  | ||||
| 	"github.com/aykhans/dodo/config" | ||||
| 	customerrors "github.com/aykhans/dodo/custom_errors" | ||||
| ) | ||||
|  | ||||
| func JSONConfigReader(filePath string) (*config.JSONConfig, error) { | ||||
| 	data, err := os.ReadFile(filePath) | ||||
| 	if err != nil { | ||||
| 		return nil, customerrors.OSErrorFormater(err) | ||||
| 	} | ||||
| 	jsonConf := config.NewJSONConfig( | ||||
| 		config.NewConfig("", 0, 0, 0, nil), | ||||
| 		nil, nil, nil, nil, nil, | ||||
| 	) | ||||
| 	err = json.Unmarshal(data, &jsonConf) | ||||
|  | ||||
| 	if err != nil { | ||||
| 		switch err := err.(type) { | ||||
| 		case *json.UnmarshalTypeError: | ||||
| 			return nil, | ||||
| 				customerrors.NewTypeError( | ||||
| 					err.Type.String(), | ||||
| 					err.Value, | ||||
| 					err.Field, | ||||
| 					err, | ||||
| 				) | ||||
| 		} | ||||
| 		return nil, customerrors.NewInvalidFileError(filePath, err) | ||||
| 	} | ||||
| 	return jsonConf, nil | ||||
| } | ||||
| @@ -2,14 +2,11 @@ package requests | ||||
|  | ||||
| import ( | ||||
| 	"context" | ||||
| 	"fmt" | ||||
| 	"errors" | ||||
| 	"math/rand" | ||||
| 	"net/url" | ||||
| 	"sync" | ||||
| 	"time" | ||||
|  | ||||
| 	"github.com/aykhans/dodo/config" | ||||
| 	"github.com/aykhans/dodo/readers" | ||||
| 	"github.com/aykhans/dodo/utils" | ||||
| 	"github.com/valyala/fasthttp" | ||||
| 	"github.com/valyala/fasthttp/fasthttpproxy" | ||||
| @@ -22,80 +19,38 @@ type ClientGeneratorFunc func() *fasthttp.HostClient | ||||
| func getClients( | ||||
| 	ctx context.Context, | ||||
| 	timeout time.Duration, | ||||
| 	proxies []config.Proxy, | ||||
| 	dodosCount uint, | ||||
| 	proxies []url.URL, | ||||
| 	maxConns uint, | ||||
| 	yes bool, | ||||
| 	noProxyCheck bool, | ||||
| 	URL *url.URL, | ||||
| 	URL url.URL, | ||||
| ) []*fasthttp.HostClient { | ||||
| 	isTLS := URL.Scheme == "https" | ||||
|  | ||||
| 	if proxiesLen := len(proxies); proxiesLen > 0 { | ||||
| 		// If noProxyCheck is true, we will return the clients without checking the proxies. | ||||
| 		if noProxyCheck { | ||||
| 			clients := make([]*fasthttp.HostClient, 0, proxiesLen) | ||||
| 			addr := URL.Host | ||||
| 			if isTLS && URL.Port() == "" { | ||||
| 				addr += ":443" | ||||
| 			} | ||||
|  | ||||
| 			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, | ||||
| 				}, | ||||
| 				) | ||||
| 			} | ||||
| 			return clients | ||||
| 		clients := make([]*fasthttp.HostClient, 0, proxiesLen) | ||||
| 		addr := URL.Host | ||||
| 		if isTLS && URL.Port() == "" { | ||||
| 			addr += ":443" | ||||
| 		} | ||||
|  | ||||
| 		// 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, | ||||
| 				), | ||||
| 		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, | ||||
| 			}, | ||||
| 			) | ||||
| 		} | ||||
| 		if !yes { | ||||
| 			response := readers.CLIYesOrNoReader("\n"+yesOrNoMessage, yesOrNoDefault) | ||||
| 			if !response { | ||||
| 				utils.PrintAndExit("Exiting...") | ||||
| 			} | ||||
| 		} | ||||
| 		fmt.Println() | ||||
| 		if activeProxyClientsCount > 0 { | ||||
| 			return activeProxyClients | ||||
| 		} | ||||
| 		return clients | ||||
| 	} | ||||
|  | ||||
| 	client := &fasthttp.HostClient{ | ||||
| @@ -110,200 +65,19 @@ func getClients( | ||||
| 	return []*fasthttp.HostClient{client} | ||||
| } | ||||
|  | ||||
| // getActiveProxyClients divides the proxies into slices based on the number of dodos and | ||||
| // launches goroutines to find active proxy clients for each slice. | ||||
| // It uses a progress tracker to monitor the progress of the search. | ||||
| // Once all goroutines have completed, the function waits for them to finish and | ||||
| // returns a flattened slice of active proxy clients. | ||||
| func getActiveProxyClients( | ||||
| 	ctx context.Context, | ||||
| 	proxies []config.Proxy, | ||||
| 	timeout time.Duration, | ||||
| 	dodosCount uint, | ||||
| 	maxConns uint, | ||||
| 	URL *url.URL, | ||||
| ) []*fasthttp.HostClient { | ||||
| 	activeProxyClientsArray := make([][]*fasthttp.HostClient, dodosCount) | ||||
| 	proxiesCount := len(proxies) | ||||
| 	dodosCountInt := int(dodosCount) | ||||
|  | ||||
| 	var ( | ||||
| 		wg       sync.WaitGroup | ||||
| 		streamWG sync.WaitGroup | ||||
| 	) | ||||
| 	wg.Add(dodosCountInt) | ||||
| 	streamWG.Add(1) | ||||
| 	var proxiesSlice []config.Proxy | ||||
| 	increase := make(chan int64, proxiesCount) | ||||
|  | ||||
| 	streamCtx, streamCtxCancel := context.WithCancel(context.Background()) | ||||
| 	go streamProgress(streamCtx, &streamWG, int64(proxiesCount), "Searching for active proxies🌐", increase) | ||||
|  | ||||
| 	for i := range dodosCountInt { | ||||
| 		if i+1 == dodosCountInt { | ||||
| 			proxiesSlice = proxies[i*proxiesCount/dodosCountInt:] | ||||
| 		} else { | ||||
| 			proxiesSlice = proxies[i*proxiesCount/dodosCountInt : (i+1)*proxiesCount/dodosCountInt] | ||||
| 		} | ||||
| 		go findActiveProxyClients( | ||||
| 			ctx, | ||||
| 			proxiesSlice, | ||||
| 			timeout, | ||||
| 			&activeProxyClientsArray[i], | ||||
| 			increase, | ||||
| 			maxConns, | ||||
| 			URL, | ||||
| 			&wg, | ||||
| 		) | ||||
| 	} | ||||
| 	wg.Wait() | ||||
| 	streamCtxCancel() | ||||
| 	streamWG.Wait() | ||||
| 	return utils.Flatten(activeProxyClientsArray) | ||||
| } | ||||
|  | ||||
| // findActiveProxyClients checks a list of proxies to determine which ones are active | ||||
| // and appends the active ones to the provided activeProxyClients slice. | ||||
| // | ||||
| // Parameters: | ||||
| //   - ctx: The context to control cancellation and timeout. | ||||
| //   - proxies: A slice of Proxy configurations to be checked. | ||||
| //   - timeout: The duration to wait for each proxy check before timing out. | ||||
| //   - activeProxyClients: A pointer to a slice where active proxy clients will be appended. | ||||
| //   - increase: A channel to signal the increase of checked proxies count. | ||||
| //   - URL: The URL to be used for checking the proxies. | ||||
| //   - wg: A WaitGroup to signal when the function is done. | ||||
| // | ||||
| // The function sends a GET request to each proxy using the provided URL. If the proxy | ||||
| // responds with a status code of 200, it is considered active and added to the activeProxyClients slice. | ||||
| // The function respects the context's cancellation and timeout settings. | ||||
| func findActiveProxyClients( | ||||
| 	ctx context.Context, | ||||
| 	proxies []config.Proxy, | ||||
| 	timeout time.Duration, | ||||
| 	activeProxyClients *[]*fasthttp.HostClient, | ||||
| 	increase chan<- int64, | ||||
| 	maxConns uint, | ||||
| 	URL *url.URL, | ||||
| 	wg *sync.WaitGroup, | ||||
| ) { | ||||
| 	defer wg.Done() | ||||
|  | ||||
| 	request := fasthttp.AcquireRequest() | ||||
| 	defer fasthttp.ReleaseRequest(request) | ||||
| 	request.SetRequestURI(config.ProxyCheckURL) | ||||
| 	request.Header.SetMethod("GET") | ||||
|  | ||||
| 	for _, proxy := range proxies { | ||||
| 		if ctx.Err() != nil { | ||||
| 			return | ||||
| 		} | ||||
|  | ||||
| 		func() { | ||||
| 			defer func() { increase <- 1 }() | ||||
|  | ||||
| 			response := fasthttp.AcquireResponse() | ||||
| 			defer fasthttp.ReleaseResponse(response) | ||||
|  | ||||
| 			dialFunc, err := getDialFunc(&proxy, timeout) | ||||
| 			if err != nil { | ||||
| 				return | ||||
| 			} | ||||
| 			client := &fasthttp.Client{ | ||||
| 				Dial: dialFunc, | ||||
| 			} | ||||
| 			defer client.CloseIdleConnections() | ||||
|  | ||||
| 			ch := make(chan error) | ||||
| 			go func() { | ||||
| 				err := client.DoTimeout(request, response, timeout) | ||||
| 				ch <- err | ||||
| 			}() | ||||
| 			select { | ||||
| 			case err := <-ch: | ||||
| 				if err != nil { | ||||
| 					return | ||||
| 				} | ||||
| 				break | ||||
| 			case <-time.After(timeout): | ||||
| 				return | ||||
| 			case <-ctx.Done(): | ||||
| 				return | ||||
| 			} | ||||
|  | ||||
| 			isTLS := URL.Scheme == "https" | ||||
| 			addr := URL.Host | ||||
| 			if isTLS && URL.Port() == "" { | ||||
| 				addr += ":443" | ||||
| 			} | ||||
| 			if response.StatusCode() == 200 { | ||||
| 				*activeProxyClients = append( | ||||
| 					*activeProxyClients, | ||||
| 					&fasthttp.HostClient{ | ||||
| 						MaxConns:            int(maxConns), | ||||
| 						IsTLS:               isTLS, | ||||
| 						Addr:                addr, | ||||
| 						Dial:                dialFunc, | ||||
| 						MaxIdleConnDuration: timeout, | ||||
| 						MaxConnDuration:     timeout, | ||||
| 						WriteTimeout:        timeout, | ||||
| 						ReadTimeout:         timeout, | ||||
| 					}, | ||||
| 				) | ||||
| 			} | ||||
| 		}() | ||||
| 	} | ||||
| } | ||||
|  | ||||
| // getDialFunc returns a fasthttp.DialFunc based on the provided proxy configuration. | ||||
| // It takes a pointer to a config.Proxy struct as input and returns a fasthttp.DialFunc and an error. | ||||
| // The function parses the proxy URL, determines the scheme (socks5, socks5h, http, or https), | ||||
| // and creates a dialer accordingly. If the proxy URL is invalid or the scheme is not supported, | ||||
| // it returns an error. | ||||
| func getDialFunc(proxy *config.Proxy, timeout time.Duration) (fasthttp.DialFunc, error) { | ||||
| 	parsedProxyURL, err := url.Parse(proxy.URL) | ||||
| 	if err != nil { | ||||
| 		return nil, err | ||||
| 	} | ||||
|  | ||||
| // getDialFunc returns the appropriate fasthttp.DialFunc based on the provided proxy URL scheme. | ||||
| // It supports SOCKS5 ('socks5' or 'socks5h') and HTTP ('http') proxy schemes. | ||||
| // For HTTP proxies, the timeout parameter determines connection timeouts. | ||||
| // Returns an error if the proxy scheme is unsupported. | ||||
| func getDialFunc(proxy *url.URL, timeout time.Duration) (fasthttp.DialFunc, error) { | ||||
| 	var dialer fasthttp.DialFunc | ||||
| 	if parsedProxyURL.Scheme == "socks5" || parsedProxyURL.Scheme == "socks5h" { | ||||
| 		if proxy.Username != "" { | ||||
| 			dialer = fasthttpproxy.FasthttpSocksDialer( | ||||
| 				fmt.Sprintf( | ||||
| 					"%s://%s:%s@%s", | ||||
| 					parsedProxyURL.Scheme, | ||||
| 					proxy.Username, | ||||
| 					proxy.Password, | ||||
| 					parsedProxyURL.Host, | ||||
| 				), | ||||
| 			) | ||||
| 		} else { | ||||
| 			dialer = fasthttpproxy.FasthttpSocksDialer( | ||||
| 				fmt.Sprintf( | ||||
| 					"%s://%s", | ||||
| 					parsedProxyURL.Scheme, | ||||
| 					parsedProxyURL.Host, | ||||
| 				), | ||||
| 			) | ||||
| 		} | ||||
| 	} 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, | ||||
| 			) | ||||
| 		} | ||||
|  | ||||
| 	if proxy.Scheme == "socks5" || proxy.Scheme == "socks5h" { | ||||
| 		dialer = fasthttpproxy.FasthttpSocksDialerDualStack(proxy.String()) | ||||
| 	} else if proxy.Scheme == "http" { | ||||
| 		dialer = fasthttpproxy.FasthttpHTTPDialerDualStackTimeout(proxy.String(), timeout) | ||||
| 	} else { | ||||
| 		return nil, err | ||||
| 		return nil, errors.New("unsupported proxy scheme") | ||||
| 	} | ||||
| 	return dialer, nil | ||||
| } | ||||
|   | ||||
| @@ -7,7 +7,6 @@ import ( | ||||
| 	"time" | ||||
|  | ||||
| 	"github.com/jedib0t/go-pretty/v6/progress" | ||||
| 	"github.com/valyala/fasthttp" | ||||
| ) | ||||
|  | ||||
| // streamProgress streams the progress of a task to the console using a progress bar. | ||||
| @@ -18,7 +17,7 @@ import ( | ||||
| func streamProgress( | ||||
| 	ctx context.Context, | ||||
| 	wg *sync.WaitGroup, | ||||
| 	total int64, | ||||
| 	total uint, | ||||
| 	message string, | ||||
| 	increase <-chan int64, | ||||
| ) { | ||||
| @@ -28,19 +27,26 @@ func streamProgress( | ||||
| 	pw.SetStyle(progress.StyleBlocks) | ||||
| 	pw.SetTrackerLength(40) | ||||
| 	pw.SetUpdateFrequency(time.Millisecond * 250) | ||||
| 	if total == 0 { | ||||
| 		pw.Style().Visibility.Percentage = false | ||||
| 	} | ||||
| 	go pw.Render() | ||||
| 	dodosTracker := progress.Tracker{ | ||||
| 		Message: message, | ||||
| 		Total:   total, | ||||
| 		Total:   int64(total), | ||||
| 	} | ||||
| 	pw.AppendTracker(&dodosTracker) | ||||
|  | ||||
| 	for { | ||||
| 		select { | ||||
| 		case <-ctx.Done(): | ||||
| 			fmt.Printf("\r") | ||||
| 			dodosTracker.MarkAsErrored() | ||||
| 			if err := ctx.Err(); err == context.Canceled || err == context.DeadlineExceeded { | ||||
| 				dodosTracker.MarkAsDone() | ||||
| 			} else { | ||||
| 				dodosTracker.MarkAsErrored() | ||||
| 			} | ||||
| 			time.Sleep(time.Millisecond * 300) | ||||
| 			pw.Stop() | ||||
| 			fmt.Printf("\r") | ||||
| 			return | ||||
|  | ||||
| 		case value := <-increase: | ||||
| @@ -48,28 +54,3 @@ func streamProgress( | ||||
| 		} | ||||
| 	} | ||||
| } | ||||
|  | ||||
| // checkConnection checks the internet connection by making requests to different websites. | ||||
| // It returns true if the connection is successful, otherwise false. | ||||
| func checkConnection(ctx context.Context) bool { | ||||
| 	ch := make(chan bool) | ||||
| 	go func() { | ||||
| 		_, _, err := fasthttp.Get(nil, "https://www.google.com") | ||||
| 		if err != nil { | ||||
| 			_, _, err = fasthttp.Get(nil, "https://www.bing.com") | ||||
| 			if err != nil { | ||||
| 				_, _, err = fasthttp.Get(nil, "https://www.yahoo.com") | ||||
| 				ch <- err == nil | ||||
| 			} | ||||
| 			ch <- true | ||||
| 		} | ||||
| 		ch <- true | ||||
| 	}() | ||||
|  | ||||
| 	select { | ||||
| 	case <-ctx.Done(): | ||||
| 		return false | ||||
| 	case res := <-ch: | ||||
| 		return res | ||||
| 	} | ||||
| } | ||||
|   | ||||
| @@ -7,7 +7,7 @@ import ( | ||||
| 	"time" | ||||
|  | ||||
| 	"github.com/aykhans/dodo/config" | ||||
| 	customerrors "github.com/aykhans/dodo/custom_errors" | ||||
| 	"github.com/aykhans/dodo/types" | ||||
| 	"github.com/aykhans/dodo/utils" | ||||
| 	"github.com/valyala/fasthttp" | ||||
| ) | ||||
| @@ -26,7 +26,6 @@ type Request struct { | ||||
| func (r *Request) Send(ctx context.Context, timeout time.Duration) (*fasthttp.Response, error) { | ||||
| 	client := r.getClient() | ||||
| 	request := r.getRequest() | ||||
| 	defer client.CloseIdleConnections() | ||||
| 	defer fasthttp.ReleaseRequest(request) | ||||
|  | ||||
| 	response := fasthttp.AcquireResponse() | ||||
| @@ -44,9 +43,9 @@ func (r *Request) Send(ctx context.Context, timeout time.Duration) (*fasthttp.Re | ||||
| 		return response, nil | ||||
| 	case <-time.After(timeout): | ||||
| 		fasthttp.ReleaseResponse(response) | ||||
| 		return nil, customerrors.ErrTimeout | ||||
| 		return nil, types.ErrTimeout | ||||
| 	case <-ctx.Done(): | ||||
| 		return nil, customerrors.ErrInterrupt | ||||
| 		return nil, types.ErrInterrupt | ||||
| 	} | ||||
| } | ||||
|  | ||||
| @@ -75,9 +74,9 @@ func newRequest( | ||||
|  | ||||
| 	getRequest := getRequestGeneratorFunc( | ||||
| 		requestConfig.URL, | ||||
| 		requestConfig.Params, | ||||
| 		requestConfig.Headers, | ||||
| 		requestConfig.Cookies, | ||||
| 		requestConfig.Params, | ||||
| 		requestConfig.Method, | ||||
| 		requestConfig.Body, | ||||
| 		localRand, | ||||
| @@ -91,37 +90,36 @@ func newRequest( | ||||
| 	return requests | ||||
| } | ||||
|  | ||||
| // getRequestGeneratorFunc returns a RequestGeneratorFunc which generates HTTP requests | ||||
| // with the specified parameters. | ||||
| // The function uses a local random number generator to select bodies, headers, cookies, and parameters | ||||
| // if multiple options are provided. | ||||
| // getRequestGeneratorFunc returns a RequestGeneratorFunc which generates HTTP requests with the specified parameters. | ||||
| // The function uses a local random number generator to select bodies, headers, cookies, and parameters if multiple options are provided. | ||||
| func getRequestGeneratorFunc( | ||||
| 	URL *url.URL, | ||||
| 	Headers map[string][]string, | ||||
| 	Cookies map[string][]string, | ||||
| 	Params map[string][]string, | ||||
| 	Method string, | ||||
| 	Bodies []string, | ||||
| 	URL url.URL, | ||||
| 	params types.Params, | ||||
| 	headers types.Headers, | ||||
| 	cookies types.Cookies, | ||||
| 	method string, | ||||
| 	bodies []string, | ||||
| 	localRand *rand.Rand, | ||||
| ) RequestGeneratorFunc { | ||||
| 	bodiesLen := len(Bodies) | ||||
| 	bodiesLen := len(bodies) | ||||
| 	getBody := func() string { return "" } | ||||
| 	if bodiesLen == 1 { | ||||
| 		getBody = func() string { return Bodies[0] } | ||||
| 		getBody = func() string { return bodies[0] } | ||||
| 	} else if bodiesLen > 1 { | ||||
| 		getBody = utils.RandomValueCycle(Bodies, localRand) | ||||
| 		getBody = utils.RandomValueCycle(bodies, localRand) | ||||
| 	} | ||||
| 	getHeaders := getKeyValueSetFunc(Headers, localRand) | ||||
| 	getCookies := getKeyValueSetFunc(Cookies, localRand) | ||||
| 	getParams := getKeyValueSetFunc(Params, localRand) | ||||
|  | ||||
| 	getParams := getKeyValueGeneratorFunc(params, localRand) | ||||
| 	getHeaders := getKeyValueGeneratorFunc(headers, localRand) | ||||
| 	getCookies := getKeyValueGeneratorFunc(cookies, localRand) | ||||
|  | ||||
| 	return func() *fasthttp.Request { | ||||
| 		return newFasthttpRequest( | ||||
| 			URL, | ||||
| 			getParams(), | ||||
| 			getHeaders(), | ||||
| 			getCookies(), | ||||
| 			getParams(), | ||||
| 			Method, | ||||
| 			method, | ||||
| 			getBody(), | ||||
| 		) | ||||
| 	} | ||||
| @@ -130,12 +128,12 @@ func getRequestGeneratorFunc( | ||||
| // newFasthttpRequest creates a new fasthttp.Request object with the provided parameters. | ||||
| // It sets the request URI, host header, headers, cookies, params, method, and body. | ||||
| func newFasthttpRequest( | ||||
| 	URL *url.URL, | ||||
| 	Headers map[string]string, | ||||
| 	Cookies map[string]string, | ||||
| 	Params map[string]string, | ||||
| 	Method string, | ||||
| 	Body string, | ||||
| 	URL url.URL, | ||||
| 	params []types.KeyValue[string, string], | ||||
| 	headers []types.KeyValue[string, string], | ||||
| 	cookies []types.KeyValue[string, string], | ||||
| 	method string, | ||||
| 	body string, | ||||
| ) *fasthttp.Request { | ||||
| 	request := fasthttp.AcquireRequest() | ||||
| 	request.SetRequestURI(URL.Path) | ||||
| @@ -143,12 +141,12 @@ func newFasthttpRequest( | ||||
| 	// Set the host of the request to the host header | ||||
| 	// If the host header is not set, the request will fail | ||||
| 	// If there is host header in the headers, it will be overwritten | ||||
| 	request.Header.Set("Host", URL.Host) | ||||
| 	setRequestHeaders(request, Headers) | ||||
| 	setRequestCookies(request, Cookies) | ||||
| 	setRequestParams(request, Params) | ||||
| 	setRequestMethod(request, Method) | ||||
| 	setRequestBody(request, Body) | ||||
| 	request.Header.SetHost(URL.Host) | ||||
| 	setRequestParams(request, params) | ||||
| 	setRequestHeaders(request, headers) | ||||
| 	setRequestCookies(request, cookies) | ||||
| 	setRequestMethod(request, method) | ||||
| 	setRequestBody(request, body) | ||||
| 	if URL.Scheme == "https" { | ||||
| 		request.URI().SetScheme("https") | ||||
| 	} | ||||
| @@ -156,28 +154,25 @@ func newFasthttpRequest( | ||||
| 	return request | ||||
| } | ||||
|  | ||||
| // setRequestHeaders sets the headers of the given request with the provided key-value pairs. | ||||
| func setRequestHeaders(req *fasthttp.Request, headers map[string]string) { | ||||
| 	req.Header.Set("User-Agent", config.DefaultUserAgent) | ||||
| 	for key, value := range headers { | ||||
| 		req.Header.Set(key, value) | ||||
| // setRequestParams adds the query parameters of the given request based on the provided key-value pairs. | ||||
| func setRequestParams(req *fasthttp.Request, params []types.KeyValue[string, string]) { | ||||
| 	for _, param := range params { | ||||
| 		req.URI().QueryArgs().Add(param.Key, param.Value) | ||||
| 	} | ||||
| } | ||||
|  | ||||
| // setRequestCookies sets the cookies in the given request. | ||||
| func setRequestCookies(req *fasthttp.Request, cookies map[string]string) { | ||||
| 	for key, value := range cookies { | ||||
| 		req.Header.SetCookie(key, value) | ||||
| // setRequestHeaders adds the headers of the given request with the provided key-value pairs. | ||||
| func setRequestHeaders(req *fasthttp.Request, headers []types.KeyValue[string, string]) { | ||||
| 	for _, header := range headers { | ||||
| 		req.Header.Add(header.Key, header.Value) | ||||
| 	} | ||||
| } | ||||
|  | ||||
| // setRequestParams sets the query parameters of the given request based on the provided map of key-value pairs. | ||||
| func setRequestParams(req *fasthttp.Request, params map[string]string) { | ||||
| 	urlParams := url.Values{} | ||||
| 	for key, value := range params { | ||||
| 		urlParams.Add(key, value) | ||||
| // setRequestCookies adds the cookies of the given request with the provided key-value pairs. | ||||
| func setRequestCookies(req *fasthttp.Request, cookies []types.KeyValue[string, string]) { | ||||
| 	for _, cookie := range cookies { | ||||
| 		req.Header.Add("Cookie", cookie.Key+"="+cookie.Value) | ||||
| 	} | ||||
| 	req.URI().SetQueryString(urlParams.Encode()) | ||||
| } | ||||
|  | ||||
| // setRequestMethod sets the HTTP request method for the given request. | ||||
| @@ -191,59 +186,62 @@ func setRequestBody(req *fasthttp.Request, body string) { | ||||
| 	req.SetBody([]byte(body)) | ||||
| } | ||||
|  | ||||
| // getKeyValueSetFunc generates a function that returns a map of key-value pairs based on the provided key-value set. | ||||
| // The generated function will either return fixed values or random values depending on the input. | ||||
| // getKeyValueGeneratorFunc creates a function that generates key-value pairs for HTTP requests. | ||||
| // It takes a slice of key-value pairs where each key maps to a slice of possible values, | ||||
| // and a random number generator. | ||||
| // | ||||
| // Returns: | ||||
| //   - A function that returns a map of key-value pairs. If the input map contains multiple values for a key, | ||||
| //     the returned function will generate random values for that key. If the input map contains a single value | ||||
| //     for a key, the returned function will always return that value. If the input map is empty for a key, | ||||
| //     the returned function will generate an empty string for that key. | ||||
| func getKeyValueSetFunc[ | ||||
| 	KeyValueSet map[string][]string, | ||||
| 	KeyValue map[string]string, | ||||
| ](keyValueSet KeyValueSet, localRand *rand.Rand) func() KeyValue { | ||||
| // 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 { | ||||
| 	getKeyValueSlice := []map[string]func() string{} | ||||
| 	isRandom := false | ||||
| 	for key, values := range keyValueSet { | ||||
| 		valuesLen := len(values) | ||||
|  | ||||
| 		// 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 "" } | ||||
| 	for _, kv := range keyValueSlice { | ||||
| 		valuesLen := len(kv.Value) | ||||
|  | ||||
| 		getValueFunc := func() string { return "" } | ||||
| 		if valuesLen == 1 { | ||||
| 			getKeyValue = func() string { return values[0] } | ||||
| 			getValueFunc = func() string { return kv.Value[0] } | ||||
| 		} else if valuesLen > 1 { | ||||
| 			getKeyValue = utils.RandomValueCycle(values, localRand) | ||||
| 			getValueFunc = utils.RandomValueCycle(kv.Value, localRand) | ||||
| 			isRandom = true | ||||
| 		} | ||||
|  | ||||
| 		getKeyValueSlice = append( | ||||
| 			getKeyValueSlice, | ||||
| 			map[string]func() string{key: getKeyValue}, | ||||
| 			map[string]func() string{kv.Key: getValueFunc}, | ||||
| 		) | ||||
| 	} | ||||
|  | ||||
| 	// 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 { | ||||
| 		return func() T { | ||||
| 			keyValues := make(T, len(getKeyValueSlice)) | ||||
| 			for i, keyValue := range getKeyValueSlice { | ||||
| 				for key, value := range keyValue { | ||||
| 					keyValues[key] = value() | ||||
| 					keyValues[i] = types.KeyValue[string, string]{ | ||||
| 						Key:   key, | ||||
| 						Value: value(), | ||||
| 					} | ||||
| 				} | ||||
| 			} | ||||
| 			return keyValues | ||||
| 		} | ||||
| 	} else { | ||||
| 		keyValues := make(KeyValue, len(getKeyValueSlice)) | ||||
| 		for _, keyValue := range getKeyValueSlice { | ||||
| 		keyValues := make(T, len(getKeyValueSlice)) | ||||
| 		for i, keyValue := range getKeyValueSlice { | ||||
| 			for key, value := range keyValue { | ||||
| 				keyValues[key] = value() | ||||
| 				keyValues[i] = types.KeyValue[string, string]{ | ||||
| 					Key:   key, | ||||
| 					Value: value(), | ||||
| 				} | ||||
| 			} | ||||
| 		} | ||||
| 		return func() KeyValue { return keyValues } | ||||
| 		return func() T { return keyValues } | ||||
| 	} | ||||
| } | ||||
|   | ||||
| @@ -4,7 +4,7 @@ import ( | ||||
| 	"os" | ||||
| 	"time" | ||||
|  | ||||
| 	. "github.com/aykhans/dodo/types" | ||||
| 	"github.com/aykhans/dodo/types" | ||||
| 	"github.com/aykhans/dodo/utils" | ||||
| 	"github.com/jedib0t/go-pretty/v6/table" | ||||
| ) | ||||
| @@ -32,8 +32,8 @@ func (responses Responses) Print() { | ||||
| 		Min:   responses[0].Time, | ||||
| 		Max:   responses[0].Time, | ||||
| 	} | ||||
| 	mergedResponses := make(map[string]Durations) | ||||
| 	var allDurations Durations | ||||
| 	mergedResponses := make(map[string]types.Durations) | ||||
| 	var allDurations types.Durations | ||||
|  | ||||
| 	for _, response := range responses { | ||||
| 		if response.Time < total.Min { | ||||
|   | ||||
							
								
								
									
										130
									
								
								requests/run.go
									
									
									
									
									
								
							
							
						
						
									
										130
									
								
								requests/run.go
									
									
									
									
									
								
							| @@ -7,48 +7,33 @@ import ( | ||||
| 	"time" | ||||
|  | ||||
| 	"github.com/aykhans/dodo/config" | ||||
| 	customerrors "github.com/aykhans/dodo/custom_errors" | ||||
| 	"github.com/aykhans/dodo/types" | ||||
| 	"github.com/aykhans/dodo/utils" | ||||
| 	"github.com/valyala/fasthttp" | ||||
| ) | ||||
|  | ||||
| // Run executes the main logic for processing requests based on the provided configuration. | ||||
| // It first checks for an internet connection with a timeout context. If no connection is found, | ||||
| // it returns an error. Then, it initializes clients based on the request configuration and | ||||
| // releases the dodos. If the context is canceled and no responses are collected, it returns an interrupt error. | ||||
| // It initializes clients based on the request configuration and releases the dodos. | ||||
| // If the context is canceled and no responses are collected, it returns an interrupt error. | ||||
| // | ||||
| // Parameters: | ||||
| //   - ctx: The context for managing request lifecycle and cancellation. | ||||
| //   - requestConfig: The configuration for the request, including timeout, proxies, and other settings. | ||||
| // | ||||
| // Returns: | ||||
| //   - Responses: A collection of responses from the executed requests. | ||||
| //   - error: An error if the operation fails, such as no internet connection or an interrupt. | ||||
| func Run(ctx context.Context, requestConfig *config.RequestConfig) (Responses, error) { | ||||
| 	checkConnectionCtx, checkConnectionCtxCancel := context.WithTimeout(ctx, 8*time.Second) | ||||
| 	if !checkConnection(checkConnectionCtx) { | ||||
| 		checkConnectionCtxCancel() | ||||
| 		return nil, customerrors.ErrNoInternet | ||||
| 	} | ||||
| 	checkConnectionCtxCancel() | ||||
|  | ||||
| 	clients := getClients( | ||||
| 		ctx, | ||||
| 		requestConfig.Timeout, | ||||
| 		requestConfig.Proxies, | ||||
| 		requestConfig.GetValidDodosCountForProxies(), | ||||
| 		requestConfig.GetMaxConns(fasthttp.DefaultMaxConnsPerHost), | ||||
| 		requestConfig.Yes, | ||||
| 		requestConfig.NoProxyCheck, | ||||
| 		requestConfig.URL, | ||||
| 	) | ||||
| 	if clients == nil { | ||||
| 		return nil, customerrors.ErrInterrupt | ||||
| 		return nil, types.ErrInterrupt | ||||
| 	} | ||||
|  | ||||
| 	responses := releaseDodos(ctx, requestConfig, clients) | ||||
| 	if ctx.Err() != nil && len(responses) == 0 { | ||||
| 		return nil, customerrors.ErrInterrupt | ||||
| 		return nil, types.ErrInterrupt | ||||
| 	} | ||||
|  | ||||
| 	return responses, nil | ||||
| @@ -74,47 +59,59 @@ func releaseDodos( | ||||
| 		streamWG            sync.WaitGroup | ||||
| 		requestCountPerDodo uint | ||||
| 		dodosCount          uint = requestConfig.GetValidDodosCountForRequests() | ||||
| 		dodosCountInt       int  = int(dodosCount) | ||||
| 		requestCount        uint = uint(requestConfig.RequestCount) | ||||
| 		responses                = make([][]*Response, dodosCount) | ||||
| 		increase                 = make(chan int64, requestCount) | ||||
| 		increase                 = make(chan int64, requestConfig.RequestCount) | ||||
| 	) | ||||
|  | ||||
| 	wg.Add(dodosCountInt) | ||||
| 	wg.Add(int(dodosCount)) | ||||
| 	streamWG.Add(1) | ||||
| 	streamCtx, streamCtxCancel := context.WithCancel(context.Background()) | ||||
|  | ||||
| 	go streamProgress(streamCtx, &streamWG, int64(requestCount), "Dodos Working🔥", increase) | ||||
| 	go streamProgress(streamCtx, &streamWG, requestConfig.RequestCount, "Dodos Working🔥", increase) | ||||
|  | ||||
| 	for i := range dodosCount { | ||||
| 		if i+1 == dodosCount { | ||||
| 			requestCountPerDodo = requestCount - (i * requestCount / dodosCount) | ||||
| 		} else { | ||||
| 			requestCountPerDodo = ((i + 1) * requestCount / dodosCount) - | ||||
| 				(i * requestCount / dodosCount) | ||||
| 	if requestConfig.RequestCount == 0 { | ||||
| 		for i := range dodosCount { | ||||
| 			go sendRequest( | ||||
| 				ctx, | ||||
| 				newRequest(*requestConfig, clients, int64(i)), | ||||
| 				requestConfig.Timeout, | ||||
| 				&responses[i], | ||||
| 				increase, | ||||
| 				&wg, | ||||
| 			) | ||||
| 		} | ||||
| 	} else { | ||||
| 		for i := range dodosCount { | ||||
| 			if i+1 == dodosCount { | ||||
| 				requestCountPerDodo = requestConfig.RequestCount - (i * requestConfig.RequestCount / dodosCount) | ||||
| 			} else { | ||||
| 				requestCountPerDodo = ((i + 1) * requestConfig.RequestCount / dodosCount) - | ||||
| 					(i * requestConfig.RequestCount / dodosCount) | ||||
| 			} | ||||
|  | ||||
| 		go sendRequest( | ||||
| 			ctx, | ||||
| 			newRequest(*requestConfig, clients, int64(i)), | ||||
| 			requestConfig.Timeout, | ||||
| 			requestCountPerDodo, | ||||
| 			&responses[i], | ||||
| 			increase, | ||||
| 			&wg, | ||||
| 		) | ||||
| 			go sendRequestByCount( | ||||
| 				ctx, | ||||
| 				newRequest(*requestConfig, clients, int64(i)), | ||||
| 				requestConfig.Timeout, | ||||
| 				requestCountPerDodo, | ||||
| 				&responses[i], | ||||
| 				increase, | ||||
| 				&wg, | ||||
| 			) | ||||
| 		} | ||||
| 	} | ||||
|  | ||||
| 	wg.Wait() | ||||
| 	streamCtxCancel() | ||||
| 	streamWG.Wait() | ||||
| 	return utils.Flatten(responses) | ||||
| } | ||||
|  | ||||
| // sendRequest sends a specified number of HTTP requests concurrently with a given timeout. | ||||
| // sendRequestByCount sends a specified number of HTTP requests concurrently with a given timeout. | ||||
| // It appends the responses to the provided responseData slice and sends the count of completed requests | ||||
| // to the increase channel. The function terminates early if the context is canceled or if a custom | ||||
| // interrupt error is encountered. | ||||
| func sendRequest( | ||||
| func sendRequestByCount( | ||||
| 	ctx context.Context, | ||||
| 	request *Request, | ||||
| 	timeout time.Duration, | ||||
| @@ -139,7 +136,54 @@ func sendRequest( | ||||
| 			} | ||||
|  | ||||
| 			if err != nil { | ||||
| 				if err == customerrors.ErrInterrupt { | ||||
| 				if err == types.ErrInterrupt { | ||||
| 					return | ||||
| 				} | ||||
| 				*responseData = append(*responseData, &Response{ | ||||
| 					Response: err.Error(), | ||||
| 					Time:     completedTime, | ||||
| 				}) | ||||
| 				increase <- 1 | ||||
| 				return | ||||
| 			} | ||||
|  | ||||
| 			*responseData = append(*responseData, &Response{ | ||||
| 				Response: strconv.Itoa(response.StatusCode()), | ||||
| 				Time:     completedTime, | ||||
| 			}) | ||||
| 			increase <- 1 | ||||
| 		}() | ||||
| 	} | ||||
| } | ||||
|  | ||||
| // 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{ | ||||
|   | ||||
							
								
								
									
										94
									
								
								types/body.go
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										94
									
								
								types/body.go
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,94 @@ | ||||
| package types | ||||
|  | ||||
| import ( | ||||
| 	"bytes" | ||||
| 	"encoding/json" | ||||
| 	"fmt" | ||||
|  | ||||
| 	"github.com/jedib0t/go-pretty/v6/text" | ||||
| ) | ||||
|  | ||||
| type Body []string | ||||
|  | ||||
| func (body Body) String() string { | ||||
| 	var buffer bytes.Buffer | ||||
| 	if len(body) == 0 { | ||||
| 		return string(buffer.Bytes()) | ||||
| 	} | ||||
|  | ||||
| 	if len(body) == 1 { | ||||
| 		buffer.WriteString(body[0]) | ||||
| 		return string(buffer.Bytes()) | ||||
| 	} | ||||
|  | ||||
| 	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 string(buffer.Bytes()) | ||||
| } | ||||
|  | ||||
| func (body *Body) UnmarshalJSON(b []byte) error { | ||||
| 	var data any | ||||
| 	if err := json.Unmarshal(b, &data); err != nil { | ||||
| 		return err | ||||
| 	} | ||||
|  | ||||
| 	switch v := data.(type) { | ||||
| 	case string: | ||||
| 		*body = []string{v} | ||||
| 	case []any: | ||||
| 		var slice []string | ||||
| 		for _, item := range v { | ||||
| 			slice = append(slice, fmt.Sprintf("%v", item)) | ||||
| 		} | ||||
| 		*body = slice | ||||
| 	default: | ||||
| 		return fmt.Errorf("invalid type for Body: %T (should be string or []string)", v) | ||||
| 	} | ||||
|  | ||||
| 	return nil | ||||
| } | ||||
|  | ||||
| func (body *Body) UnmarshalYAML(unmarshal func(interface{}) error) error { | ||||
| 	var data any | ||||
| 	if err := unmarshal(&data); err != nil { | ||||
| 		return err | ||||
| 	} | ||||
|  | ||||
| 	switch v := data.(type) { | ||||
| 	case string: | ||||
| 		*body = []string{v} | ||||
| 	case []any: | ||||
| 		var slice []string | ||||
| 		for _, item := range v { | ||||
| 			slice = append(slice, fmt.Sprintf("%v", item)) | ||||
| 		} | ||||
| 		*body = slice | ||||
| 	default: | ||||
| 		return fmt.Errorf("invalid type for Body: %T (should be string or []string)", v) | ||||
| 	} | ||||
|  | ||||
| 	return nil | ||||
| } | ||||
|  | ||||
| func (body *Body) Set(value string) error { | ||||
| 	*body = append(*body, value) | ||||
| 	return nil | ||||
| } | ||||
							
								
								
									
										32
									
								
								types/config_file.go
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										32
									
								
								types/config_file.go
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,32 @@ | ||||
| package types | ||||
|  | ||||
| import "strings" | ||||
|  | ||||
| type FileLocationType int | ||||
|  | ||||
| const ( | ||||
| 	FileLocationTypeLocal FileLocationType = iota | ||||
| 	FileLocationTypeRemoteHTTP | ||||
| ) | ||||
|  | ||||
| type ConfigFile string | ||||
|  | ||||
| func (configFile ConfigFile) String() string { | ||||
| 	return string(configFile) | ||||
| } | ||||
|  | ||||
| func (configFile ConfigFile) LocationType() FileLocationType { | ||||
| 	if strings.HasPrefix(string(configFile), "http://") || strings.HasPrefix(string(configFile), "https://") { | ||||
| 		return FileLocationTypeRemoteHTTP | ||||
| 	} | ||||
| 	return FileLocationTypeLocal | ||||
| } | ||||
|  | ||||
| func (configFile ConfigFile) Extension() string { | ||||
| 	i := strings.LastIndex(configFile.String(), ".") | ||||
| 	if i == -1 { | ||||
| 		return "" | ||||
| 	} | ||||
|  | ||||
| 	return configFile.String()[i+1:] | ||||
| } | ||||
							
								
								
									
										139
									
								
								types/cookies.go
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										139
									
								
								types/cookies.go
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,139 @@ | ||||
| package types | ||||
|  | ||||
| import ( | ||||
| 	"bytes" | ||||
| 	"encoding/json" | ||||
| 	"fmt" | ||||
| 	"strings" | ||||
|  | ||||
| 	"github.com/jedib0t/go-pretty/v6/text" | ||||
| ) | ||||
|  | ||||
| type Cookies []KeyValue[string, []string] | ||||
|  | ||||
| func (cookies Cookies) String() string { | ||||
| 	var buffer bytes.Buffer | ||||
| 	if len(cookies) == 0 { | ||||
| 		return string(buffer.Bytes()) | ||||
| 	} | ||||
|  | ||||
| 	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 string(buffer.Bytes()) | ||||
| } | ||||
|  | ||||
| func (cookies *Cookies) AppendByKey(key, value string) { | ||||
| 	if item := cookies.GetValue(key); item != nil { | ||||
| 		*item = append(*item, value) | ||||
| 	} else { | ||||
| 		*cookies = append(*cookies, KeyValue[string, []string]{Key: key, Value: []string{value}}) | ||||
| 	} | ||||
| } | ||||
|  | ||||
| func (cookies Cookies) GetValue(key string) *[]string { | ||||
| 	for i := range cookies { | ||||
| 		if cookies[i].Key == key { | ||||
| 			return &cookies[i].Value | ||||
| 		} | ||||
| 	} | ||||
| 	return nil | ||||
| } | ||||
|  | ||||
| func (cookies *Cookies) UnmarshalJSON(b []byte) error { | ||||
| 	var data []map[string]any | ||||
| 	if err := json.Unmarshal(b, &data); err != nil { | ||||
| 		return err | ||||
| 	} | ||||
|  | ||||
| 	for _, item := range data { | ||||
| 		for key, value := range item { | ||||
| 			switch parsedValue := value.(type) { | ||||
| 			case string: | ||||
| 				*cookies = append(*cookies, KeyValue[string, []string]{Key: key, Value: []string{parsedValue}}) | ||||
| 			case []any: | ||||
| 				parsedStr := make([]string, len(parsedValue)) | ||||
| 				for i, item := range parsedValue { | ||||
| 					parsedStr[i] = fmt.Sprintf("%v", item) | ||||
| 				} | ||||
| 				*cookies = append(*cookies, KeyValue[string, []string]{Key: key, Value: parsedStr}) | ||||
| 			default: | ||||
| 				return fmt.Errorf("unsupported type for cookies expected string or []string, got %T", parsedValue) | ||||
| 			} | ||||
| 		} | ||||
| 	} | ||||
|  | ||||
| 	return nil | ||||
| } | ||||
|  | ||||
| func (cookies *Cookies) UnmarshalYAML(unmarshal func(interface{}) error) error { | ||||
| 	var raw []map[string]any | ||||
| 	if err := unmarshal(&raw); err != nil { | ||||
| 		return err | ||||
| 	} | ||||
|  | ||||
| 	for _, param := range raw { | ||||
| 		for key, value := range param { | ||||
| 			switch parsed := value.(type) { | ||||
| 			case string: | ||||
| 				*cookies = append(*cookies, KeyValue[string, []string]{Key: key, Value: []string{parsed}}) | ||||
| 			case []any: | ||||
| 				var values []string | ||||
| 				for _, v := range parsed { | ||||
| 					if str, ok := v.(string); ok { | ||||
| 						values = append(values, str) | ||||
| 					} | ||||
| 				} | ||||
| 				*cookies = append(*cookies, KeyValue[string, []string]{Key: key, Value: values}) | ||||
| 			} | ||||
| 		} | ||||
| 	} | ||||
| 	return nil | ||||
| } | ||||
|  | ||||
| func (cookies *Cookies) Set(value string) error { | ||||
| 	parts := strings.SplitN(value, "=", 2) | ||||
| 	switch len(parts) { | ||||
| 	case 0: | ||||
| 		cookies.AppendByKey("", "") | ||||
| 	case 1: | ||||
| 		cookies.AppendByKey(parts[0], "") | ||||
| 	case 2: | ||||
| 		cookies.AppendByKey(parts[0], parts[1]) | ||||
| 	} | ||||
|  | ||||
| 	return nil | ||||
| } | ||||
							
								
								
									
										57
									
								
								types/duration.go
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										57
									
								
								types/duration.go
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,57 @@ | ||||
| package types | ||||
|  | ||||
| import ( | ||||
| 	"encoding/json" | ||||
| 	"errors" | ||||
| 	"time" | ||||
| ) | ||||
|  | ||||
| type Duration struct { | ||||
| 	time.Duration | ||||
| } | ||||
|  | ||||
| func (duration *Duration) UnmarshalJSON(b []byte) error { | ||||
| 	var v any | ||||
| 	if err := json.Unmarshal(b, &v); err != nil { | ||||
| 		return err | ||||
| 	} | ||||
| 	switch value := v.(type) { | ||||
| 	case float64: | ||||
| 		duration.Duration = time.Duration(value) | ||||
| 		return nil | ||||
| 	case string: | ||||
| 		var err error | ||||
| 		duration.Duration, err = time.ParseDuration(value) | ||||
| 		if err != nil { | ||||
| 			return errors.New("Duration is invalid (e.g. 400ms, 1s, 5m, 1h)") | ||||
| 		} | ||||
| 		return nil | ||||
| 	default: | ||||
| 		return errors.New("Duration is invalid (e.g. 400ms, 1s, 5m, 1h)") | ||||
| 	} | ||||
| } | ||||
|  | ||||
| func (duration Duration) MarshalJSON() ([]byte, error) { | ||||
| 	return json.Marshal(duration.Duration.String()) | ||||
| } | ||||
|  | ||||
| func (duration *Duration) UnmarshalYAML(unmarshal func(interface{}) error) error { | ||||
| 	var v any | ||||
| 	if err := unmarshal(&v); err != nil { | ||||
| 		return err | ||||
| 	} | ||||
| 	switch value := v.(type) { | ||||
| 	case float64: | ||||
| 		duration.Duration = time.Duration(value) | ||||
| 		return nil | ||||
| 	case string: | ||||
| 		var err error | ||||
| 		duration.Duration, err = time.ParseDuration(value) | ||||
| 		if err != nil { | ||||
| 			return errors.New("Duration is invalid (e.g. 400ms, 1s, 5m, 1h)") | ||||
| 		} | ||||
| 		return nil | ||||
| 	default: | ||||
| 		return errors.New("Duration is invalid (e.g. 400ms, 1s, 5m, 1h)") | ||||
| 	} | ||||
| } | ||||
							
								
								
									
										10
									
								
								types/errors.go
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										10
									
								
								types/errors.go
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,10 @@ | ||||
| package types | ||||
|  | ||||
| import ( | ||||
| 	"errors" | ||||
| ) | ||||
|  | ||||
| var ( | ||||
| 	ErrInterrupt = errors.New("interrupted") | ||||
| 	ErrTimeout   = errors.New("timeout") | ||||
| ) | ||||
							
								
								
									
										156
									
								
								types/headers.go
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										156
									
								
								types/headers.go
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,156 @@ | ||||
| package types | ||||
|  | ||||
| import ( | ||||
| 	"bytes" | ||||
| 	"encoding/json" | ||||
| 	"fmt" | ||||
| 	"strings" | ||||
|  | ||||
| 	"github.com/jedib0t/go-pretty/v6/text" | ||||
| ) | ||||
|  | ||||
| type Headers []KeyValue[string, []string] | ||||
|  | ||||
| func (headers Headers) String() string { | ||||
| 	var buffer bytes.Buffer | ||||
| 	if len(headers) == 0 { | ||||
| 		return string(buffer.Bytes()) | ||||
| 	} | ||||
|  | ||||
| 	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 string(buffer.Bytes()) | ||||
| } | ||||
|  | ||||
| func (headers *Headers) AppendByKey(key, value string) { | ||||
| 	if item := headers.GetValue(key); item != nil { | ||||
| 		*item = append(*item, value) | ||||
| 	} else { | ||||
| 		*headers = append(*headers, KeyValue[string, []string]{Key: key, Value: []string{value}}) | ||||
| 	} | ||||
| } | ||||
|  | ||||
| func (headers Headers) GetValue(key string) *[]string { | ||||
| 	for i := range headers { | ||||
| 		if headers[i].Key == key { | ||||
| 			return &headers[i].Value | ||||
| 		} | ||||
| 	} | ||||
| 	return nil | ||||
| } | ||||
|  | ||||
| func (headers Headers) Has(key string) bool { | ||||
| 	for i := range headers { | ||||
| 		if headers[i].Key == key { | ||||
| 			return true | ||||
| 		} | ||||
| 	} | ||||
| 	return false | ||||
| } | ||||
|  | ||||
| func (headers *Headers) UnmarshalJSON(b []byte) error { | ||||
| 	var data []map[string]any | ||||
| 	if err := json.Unmarshal(b, &data); err != nil { | ||||
| 		return err | ||||
| 	} | ||||
|  | ||||
| 	for _, item := range data { | ||||
| 		for key, value := range item { | ||||
| 			switch parsedValue := value.(type) { | ||||
| 			case string: | ||||
| 				*headers = append(*headers, KeyValue[string, []string]{Key: key, Value: []string{parsedValue}}) | ||||
| 			case []any: | ||||
| 				parsedStr := make([]string, len(parsedValue)) | ||||
| 				for i, item := range parsedValue { | ||||
| 					parsedStr[i] = fmt.Sprintf("%v", item) | ||||
| 				} | ||||
| 				*headers = append(*headers, KeyValue[string, []string]{Key: key, Value: parsedStr}) | ||||
| 			default: | ||||
| 				return fmt.Errorf("unsupported type for headers expected string or []string, got %T", parsedValue) | ||||
| 			} | ||||
| 		} | ||||
| 	} | ||||
|  | ||||
| 	return nil | ||||
| } | ||||
|  | ||||
| func (headers *Headers) UnmarshalYAML(unmarshal func(interface{}) error) error { | ||||
| 	var raw []map[string]any | ||||
| 	if err := unmarshal(&raw); err != nil { | ||||
| 		return err | ||||
| 	} | ||||
|  | ||||
| 	for _, param := range raw { | ||||
| 		for key, value := range param { | ||||
| 			switch parsed := value.(type) { | ||||
| 			case string: | ||||
| 				*headers = append(*headers, KeyValue[string, []string]{Key: key, Value: []string{parsed}}) | ||||
| 			case []any: | ||||
| 				var values []string | ||||
| 				for _, v := range parsed { | ||||
| 					if str, ok := v.(string); ok { | ||||
| 						values = append(values, str) | ||||
| 					} | ||||
| 				} | ||||
| 				*headers = append(*headers, KeyValue[string, []string]{Key: key, Value: values}) | ||||
| 			} | ||||
| 		} | ||||
| 	} | ||||
| 	return nil | ||||
| } | ||||
|  | ||||
| func (headers *Headers) Set(value string) error { | ||||
| 	parts := strings.SplitN(value, ":", 2) | ||||
| 	switch len(parts) { | ||||
| 	case 0: | ||||
| 		headers.AppendByKey("", "") | ||||
| 	case 1: | ||||
| 		headers.AppendByKey(parts[0], "") | ||||
| 	case 2: | ||||
| 		headers.AppendByKey(parts[0], parts[1]) | ||||
| 	} | ||||
|  | ||||
| 	return nil | ||||
| } | ||||
|  | ||||
| func (headers *Headers) SetIfNotExists(key string, value string) bool { | ||||
| 	if headers.Has(key) { | ||||
| 		return false | ||||
| 	} | ||||
| 	*headers = append(*headers, KeyValue[string, []string]{Key: key, Value: []string{value}}) | ||||
| 	return true | ||||
| } | ||||
							
								
								
									
										6
									
								
								types/key_value.go
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										6
									
								
								types/key_value.go
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,6 @@ | ||||
| package types | ||||
|  | ||||
| type KeyValue[K comparable, V any] struct { | ||||
| 	Key   K | ||||
| 	Value V | ||||
| } | ||||
| @@ -1,89 +0,0 @@ | ||||
| package types | ||||
|  | ||||
| import ( | ||||
| 	"encoding/json" | ||||
| 	"errors" | ||||
| ) | ||||
|  | ||||
| type NonNilT interface { | ||||
| 	~int | ~float64 | ~string | ~bool | ||||
| } | ||||
|  | ||||
| type Option[T NonNilT] interface { | ||||
| 	IsNone() bool | ||||
| 	ValueOrErr() (T, error) | ||||
| 	ValueOr(def T) T | ||||
| 	ValueOrPanic() T | ||||
| 	SetValue(value T) | ||||
| 	SetNone() | ||||
| 	UnmarshalJSON(data []byte) error | ||||
| } | ||||
|  | ||||
| // Don't call this struct directly, use NewOption[T] or NewNoneOption[T] instead. | ||||
| type option[T NonNilT] struct { | ||||
| 	// value holds the actual value of the Option if it is not None. | ||||
| 	value T | ||||
| 	// none indicates whether the Option is None (i.e., has no value). | ||||
| 	none bool | ||||
| } | ||||
|  | ||||
| func (o *option[T]) IsNone() bool { | ||||
| 	return o.none | ||||
| } | ||||
|  | ||||
| // If the Option is None, it will return zero value of the type and an error. | ||||
| func (o *option[T]) ValueOrErr() (T, error) { | ||||
| 	if o.IsNone() { | ||||
| 		return o.value, errors.New("Option is None") | ||||
| 	} | ||||
| 	return o.value, nil | ||||
| } | ||||
|  | ||||
| // If the Option is None, it will return the default value. | ||||
| func (o *option[T]) ValueOr(def T) T { | ||||
| 	if o.IsNone() { | ||||
| 		return def | ||||
| 	} | ||||
| 	return o.value | ||||
| } | ||||
|  | ||||
| // If the Option is None, it will panic. | ||||
| func (o *option[T]) ValueOrPanic() T { | ||||
| 	if o.IsNone() { | ||||
| 		panic("Option is None") | ||||
| 	} | ||||
| 	return o.value | ||||
| } | ||||
|  | ||||
| func (o *option[T]) SetValue(value T) { | ||||
| 	o.value = value | ||||
| 	o.none = false | ||||
| } | ||||
|  | ||||
| func (o *option[T]) SetNone() { | ||||
| 	var zeroValue T | ||||
| 	o.value = zeroValue | ||||
| 	o.none = true | ||||
| } | ||||
|  | ||||
| func (o *option[T]) UnmarshalJSON(data []byte) error { | ||||
| 	if string(data) == "null" || len(data) == 0 { | ||||
| 		o.SetNone() | ||||
| 		return nil | ||||
| 	} | ||||
|  | ||||
| 	if err := json.Unmarshal(data, &o.value); err != nil { | ||||
| 		o.SetNone() | ||||
| 		return err | ||||
| 	} | ||||
| 	o.none = false | ||||
| 	return nil | ||||
| } | ||||
|  | ||||
| func NewOption[T NonNilT](value T) *option[T] { | ||||
| 	return &option[T]{value: value} | ||||
| } | ||||
|  | ||||
| func NewNoneOption[T NonNilT]() *option[T] { | ||||
| 	return &option[T]{none: true} | ||||
| } | ||||
							
								
								
									
										139
									
								
								types/params.go
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										139
									
								
								types/params.go
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,139 @@ | ||||
| package types | ||||
|  | ||||
| import ( | ||||
| 	"bytes" | ||||
| 	"encoding/json" | ||||
| 	"fmt" | ||||
| 	"strings" | ||||
|  | ||||
| 	"github.com/jedib0t/go-pretty/v6/text" | ||||
| ) | ||||
|  | ||||
| type Params []KeyValue[string, []string] | ||||
|  | ||||
| func (params Params) String() string { | ||||
| 	var buffer bytes.Buffer | ||||
| 	if len(params) == 0 { | ||||
| 		return string(buffer.Bytes()) | ||||
| 	} | ||||
|  | ||||
| 	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 string(buffer.Bytes()) | ||||
| } | ||||
|  | ||||
| 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(interface{}) error) error { | ||||
| 	var raw []map[string]any | ||||
| 	if err := unmarshal(&raw); err != nil { | ||||
| 		return err | ||||
| 	} | ||||
|  | ||||
| 	for _, param := range raw { | ||||
| 		for key, value := range param { | ||||
| 			switch parsed := value.(type) { | ||||
| 			case string: | ||||
| 				*params = append(*params, KeyValue[string, []string]{Key: key, Value: []string{parsed}}) | ||||
| 			case []any: | ||||
| 				var values []string | ||||
| 				for _, v := range parsed { | ||||
| 					if str, ok := v.(string); ok { | ||||
| 						values = append(values, str) | ||||
| 					} | ||||
| 				} | ||||
| 				*params = append(*params, KeyValue[string, []string]{Key: key, Value: values}) | ||||
| 			} | ||||
| 		} | ||||
| 	} | ||||
| 	return nil | ||||
| } | ||||
|  | ||||
| func (params *Params) Set(value string) error { | ||||
| 	parts := strings.SplitN(value, "=", 2) | ||||
| 	switch len(parts) { | ||||
| 	case 0: | ||||
| 		params.AppendByKey("", "") | ||||
| 	case 1: | ||||
| 		params.AppendByKey(parts[0], "") | ||||
| 	case 2: | ||||
| 		params.AppendByKey(parts[0], parts[1]) | ||||
| 	} | ||||
|  | ||||
| 	return nil | ||||
| } | ||||
							
								
								
									
										116
									
								
								types/proxies.go
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										116
									
								
								types/proxies.go
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,116 @@ | ||||
| package types | ||||
|  | ||||
| import ( | ||||
| 	"bytes" | ||||
| 	"encoding/json" | ||||
| 	"fmt" | ||||
| 	"net/url" | ||||
|  | ||||
| 	"github.com/jedib0t/go-pretty/v6/text" | ||||
| ) | ||||
|  | ||||
| type Proxies []url.URL | ||||
|  | ||||
| func (proxies Proxies) String() string { | ||||
| 	var buffer bytes.Buffer | ||||
| 	if len(proxies) == 0 { | ||||
| 		return string(buffer.Bytes()) | ||||
| 	} | ||||
|  | ||||
| 	if len(proxies) == 1 { | ||||
| 		buffer.WriteString(proxies[0].String()) | ||||
| 		return string(buffer.Bytes()) | ||||
| 	} | ||||
|  | ||||
| 	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 string(buffer.Bytes()) | ||||
| } | ||||
|  | ||||
| func (proxies *Proxies) UnmarshalJSON(b []byte) error { | ||||
| 	var data any | ||||
| 	if err := json.Unmarshal(b, &data); err != nil { | ||||
| 		return err | ||||
| 	} | ||||
|  | ||||
| 	switch v := data.(type) { | ||||
| 	case string: | ||||
| 		parsed, err := url.Parse(v) | ||||
| 		if err != nil { | ||||
| 			return err | ||||
| 		} | ||||
| 		*proxies = []url.URL{*parsed} | ||||
| 	case []any: | ||||
| 		var urls []url.URL | ||||
| 		for _, item := range v { | ||||
| 			url, err := url.Parse(item.(string)) | ||||
| 			if err != nil { | ||||
| 				return err | ||||
| 			} | ||||
| 			urls = append(urls, *url) | ||||
| 		} | ||||
| 		*proxies = urls | ||||
| 	default: | ||||
| 		return fmt.Errorf("invalid type for Body: %T (should be URL or []URL)", v) | ||||
| 	} | ||||
|  | ||||
| 	return nil | ||||
| } | ||||
|  | ||||
| func (proxies *Proxies) UnmarshalYAML(unmarshal func(interface{}) error) error { | ||||
| 	var data any | ||||
| 	if err := unmarshal(&data); err != nil { | ||||
| 		return err | ||||
| 	} | ||||
|  | ||||
| 	switch v := data.(type) { | ||||
| 	case string: | ||||
| 		parsed, err := url.Parse(v) | ||||
| 		if err != nil { | ||||
| 			return err | ||||
| 		} | ||||
| 		*proxies = []url.URL{*parsed} | ||||
| 	case []any: | ||||
| 		var urls []url.URL | ||||
| 		for _, item := range v { | ||||
| 			url, err := url.Parse(item.(string)) | ||||
| 			if err != nil { | ||||
| 				return err | ||||
| 			} | ||||
| 			urls = append(urls, *url) | ||||
| 		} | ||||
| 		*proxies = urls | ||||
| 	default: | ||||
| 		return fmt.Errorf("invalid type for Body: %T (should be URL or []URL)", v) | ||||
| 	} | ||||
|  | ||||
| 	return nil | ||||
| } | ||||
|  | ||||
| func (proxies *Proxies) Set(value string) error { | ||||
| 	parsedURL, err := url.Parse(value) | ||||
| 	if err != nil { | ||||
| 		return err | ||||
| 	} | ||||
|  | ||||
| 	*proxies = append(*proxies, *parsedURL) | ||||
| 	return nil | ||||
| } | ||||
							
								
								
									
										59
									
								
								types/request_url.go
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										59
									
								
								types/request_url.go
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,59 @@ | ||||
| package types | ||||
|  | ||||
| import ( | ||||
| 	"encoding/json" | ||||
| 	"errors" | ||||
| 	"net/url" | ||||
| ) | ||||
|  | ||||
| type RequestURL struct { | ||||
| 	url.URL | ||||
| } | ||||
|  | ||||
| func (requestURL *RequestURL) UnmarshalJSON(data []byte) error { | ||||
| 	var urlStr string | ||||
| 	if err := json.Unmarshal(data, &urlStr); err != nil { | ||||
| 		return err | ||||
| 	} | ||||
|  | ||||
| 	parsedURL, err := url.Parse(urlStr) | ||||
| 	if err != nil { | ||||
| 		return errors.New("Request URL is invalid") | ||||
| 	} | ||||
|  | ||||
| 	requestURL.URL = *parsedURL | ||||
| 	return nil | ||||
| } | ||||
|  | ||||
| func (requestURL *RequestURL) UnmarshalYAML(unmarshal func(interface{}) error) error { | ||||
| 	var urlStr string | ||||
| 	if err := unmarshal(&urlStr); err != nil { | ||||
| 		return err | ||||
| 	} | ||||
|  | ||||
| 	parsedURL, err := url.Parse(urlStr) | ||||
| 	if err != nil { | ||||
| 		return errors.New("Request URL is invalid") | ||||
| 	} | ||||
|  | ||||
| 	requestURL.URL = *parsedURL | ||||
| 	return nil | ||||
| } | ||||
|  | ||||
| func (requestURL RequestURL) MarshalJSON() ([]byte, error) { | ||||
| 	return json.Marshal(requestURL.URL.String()) | ||||
| } | ||||
|  | ||||
| func (requestURL RequestURL) String() string { | ||||
| 	return requestURL.URL.String() | ||||
| } | ||||
|  | ||||
| func (requestURL *RequestURL) Set(value string) error { | ||||
| 	parsedURL, err := url.Parse(value) | ||||
| 	if err != nil { | ||||
| 		return err | ||||
| 	} | ||||
|  | ||||
| 	requestURL.URL = *parsedURL | ||||
| 	return nil | ||||
| } | ||||
							
								
								
									
										57
									
								
								types/timeout.go
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										57
									
								
								types/timeout.go
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,57 @@ | ||||
| package types | ||||
|  | ||||
| import ( | ||||
| 	"encoding/json" | ||||
| 	"errors" | ||||
| 	"time" | ||||
| ) | ||||
|  | ||||
| type Timeout struct { | ||||
| 	time.Duration | ||||
| } | ||||
|  | ||||
| func (timeout *Timeout) UnmarshalJSON(b []byte) error { | ||||
| 	var v any | ||||
| 	if err := json.Unmarshal(b, &v); err != nil { | ||||
| 		return err | ||||
| 	} | ||||
| 	switch value := v.(type) { | ||||
| 	case float64: | ||||
| 		timeout.Duration = time.Duration(value) | ||||
| 		return nil | ||||
| 	case string: | ||||
| 		var err error | ||||
| 		timeout.Duration, err = time.ParseDuration(value) | ||||
| 		if err != nil { | ||||
| 			return errors.New("Timeout is invalid (e.g. 400ms, 1s, 5m, 1h)") | ||||
| 		} | ||||
| 		return nil | ||||
| 	default: | ||||
| 		return errors.New("Timeout is invalid (e.g. 400ms, 1s, 5m, 1h)") | ||||
| 	} | ||||
| } | ||||
|  | ||||
| func (timeout Timeout) MarshalJSON() ([]byte, error) { | ||||
| 	return json.Marshal(timeout.Duration.String()) | ||||
| } | ||||
|  | ||||
| func (timeout *Timeout) UnmarshalYAML(unmarshal func(interface{}) error) error { | ||||
| 	var v any | ||||
| 	if err := unmarshal(&v); err != nil { | ||||
| 		return err | ||||
| 	} | ||||
| 	switch value := v.(type) { | ||||
| 	case float64: | ||||
| 		timeout.Duration = time.Duration(value) | ||||
| 		return nil | ||||
| 	case string: | ||||
| 		var err error | ||||
| 		timeout.Duration, err = time.ParseDuration(value) | ||||
| 		if err != nil { | ||||
| 			return errors.New("Timeout is invalid (e.g. 400ms, 1s, 5m, 1h)") | ||||
| 		} | ||||
| 		return nil | ||||
| 	default: | ||||
| 		return errors.New("Timeout is invalid (e.g. 400ms, 1s, 5m, 1h)") | ||||
| 	} | ||||
| } | ||||
							
								
								
									
										14
									
								
								utils/compare.go
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										14
									
								
								utils/compare.go
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,14 @@ | ||||
| package utils | ||||
|  | ||||
| func IsNilOrZero[T comparable](value *T) bool { | ||||
| 	if value == nil { | ||||
| 		return true | ||||
| 	} | ||||
|  | ||||
| 	var zero T | ||||
| 	if *value == zero { | ||||
| 		return true | ||||
| 	} | ||||
|  | ||||
| 	return false | ||||
| } | ||||
| @@ -1,85 +1,5 @@ | ||||
| package utils | ||||
|  | ||||
| import ( | ||||
| 	"encoding/json" | ||||
| 	"fmt" | ||||
| 	"reflect" | ||||
| ) | ||||
|  | ||||
| type TruncatedMarshaller struct { | ||||
| 	Value    interface{} | ||||
| 	MaxItems int | ||||
| } | ||||
|  | ||||
| func (t TruncatedMarshaller) MarshalJSON() ([]byte, error) { | ||||
| 	val := reflect.ValueOf(t.Value) | ||||
|  | ||||
| 	if val.Kind() != reflect.Slice && val.Kind() != reflect.Array { | ||||
| 		return json.Marshal(t.Value) | ||||
| 	} | ||||
| 	if val.Len() == 0 { | ||||
| 		return []byte("[]"), nil | ||||
| 	} | ||||
|  | ||||
| 	length := val.Len() | ||||
| 	if length <= t.MaxItems { | ||||
| 		return json.Marshal(t.Value) | ||||
| 	} | ||||
|  | ||||
| 	truncated := make([]interface{}, t.MaxItems+1) | ||||
|  | ||||
| 	for i := 0; i < t.MaxItems; i++ { | ||||
| 		truncated[i] = val.Index(i).Interface() | ||||
| 	} | ||||
|  | ||||
| 	remaining := length - t.MaxItems | ||||
| 	truncated[t.MaxItems] = fmt.Sprintf("+%d", remaining) | ||||
|  | ||||
| 	return json.Marshal(truncated) | ||||
| } | ||||
|  | ||||
| func PrettyJSONMarshal(v interface{}, maxItems int, prefix, indent string) []byte { | ||||
| 	truncated := processValue(v, maxItems) | ||||
| 	d, _ := json.MarshalIndent(truncated, prefix, indent) | ||||
| 	return d | ||||
| } | ||||
|  | ||||
| func processValue(v interface{}, maxItems int) interface{} { | ||||
| 	val := reflect.ValueOf(v) | ||||
|  | ||||
| 	switch val.Kind() { | ||||
| 	case reflect.Map: | ||||
| 		newMap := make(map[string]interface{}) | ||||
| 		iter := val.MapRange() | ||||
| 		for iter.Next() { | ||||
| 			k := iter.Key().String() | ||||
| 			newMap[k] = processValue(iter.Value().Interface(), maxItems) | ||||
| 		} | ||||
| 		return newMap | ||||
|  | ||||
| 	case reflect.Slice, reflect.Array: | ||||
| 		return TruncatedMarshaller{Value: v, MaxItems: maxItems} | ||||
|  | ||||
| 	case reflect.Struct: | ||||
| 		newMap := make(map[string]interface{}) | ||||
| 		t := val.Type() | ||||
| 		for i := 0; i < t.NumField(); i++ { | ||||
| 			field := t.Field(i) | ||||
| 			if field.IsExported() { | ||||
| 				jsonTag := field.Tag.Get("json") | ||||
| 				if jsonTag == "-" { | ||||
| 					continue | ||||
| 				} | ||||
| 				fieldName := field.Name | ||||
| 				if jsonTag != "" { | ||||
| 					fieldName = jsonTag | ||||
| 				} | ||||
| 				newMap[fieldName] = processValue(val.Field(i).Interface(), maxItems) | ||||
| 			} | ||||
| 		} | ||||
| 		return newMap | ||||
|  | ||||
| 	default: | ||||
| 		return v | ||||
| 	} | ||||
| func ToPtr[T any](value T) *T { | ||||
| 	return &value | ||||
| } | ||||
|   | ||||
| @@ -3,46 +3,12 @@ package utils | ||||
| import ( | ||||
| 	"fmt" | ||||
| 	"os" | ||||
|  | ||||
| 	"github.com/jedib0t/go-pretty/v6/text" | ||||
| ) | ||||
|  | ||||
| var Colors = struct { | ||||
| 	reset   string | ||||
| 	Red     string | ||||
| 	Green   string | ||||
| 	Yellow  string | ||||
| 	Orange  string | ||||
| 	Blue    string | ||||
| 	Magenta string | ||||
| 	Cyan    string | ||||
| 	Gray    string | ||||
| 	White   string | ||||
| }{ | ||||
| 	reset:   "\033[0m", | ||||
| 	Red:     "\033[31m", | ||||
| 	Green:   "\033[32m", | ||||
| 	Yellow:  "\033[33m", | ||||
| 	Orange:  "\033[38;5;208m", | ||||
| 	Blue:    "\033[34m", | ||||
| 	Magenta: "\033[35m", | ||||
| 	Cyan:    "\033[36m", | ||||
| 	Gray:    "\033[37m", | ||||
| 	White:   "\033[97m", | ||||
| } | ||||
|  | ||||
| func Colored(color string, a ...any) string { | ||||
| 	return color + fmt.Sprint(a...) + Colors.reset | ||||
| } | ||||
|  | ||||
| func PrintfC(color string, format string, a ...any) { | ||||
| 	fmt.Printf(Colored(color, format), a...) | ||||
| } | ||||
|  | ||||
| func PrintlnC(color string, a ...any) { | ||||
| 	fmt.Println(Colored(color, a...)) | ||||
| } | ||||
|  | ||||
| func PrintErr(err error) { | ||||
| 	PrintlnC(Colors.Red, err.Error()) | ||||
| 	fmt.Fprintln(os.Stderr, text.FgRed.Sprint(err.Error())) | ||||
| } | ||||
|  | ||||
| func PrintErrAndExit(err error) { | ||||
|   | ||||
| @@ -10,15 +10,6 @@ func Flatten[T any](nested [][]*T) []*T { | ||||
| 	return flattened | ||||
| } | ||||
|  | ||||
| func Contains[T comparable](slice []T, item T) bool { | ||||
| 	for _, i := range slice { | ||||
| 		if i == item { | ||||
| 			return true | ||||
| 		} | ||||
| 	} | ||||
| 	return false | ||||
| } | ||||
|  | ||||
| // RandomValueCycle returns a function that cycles through the provided slice of values | ||||
| // in a random order. Each call to the returned function will yield a value from the slice. | ||||
| // The order of values is determined by the provided random number generator. | ||||
|   | ||||
| @@ -1,59 +0,0 @@ | ||||
| package validation | ||||
|  | ||||
| import ( | ||||
| 	"reflect" | ||||
| 	"strings" | ||||
|  | ||||
| 	"github.com/go-playground/validator/v10" | ||||
| 	"golang.org/x/net/http/httpguts" | ||||
| ) | ||||
|  | ||||
| // net/http/request.go/isNotToken | ||||
| func isNotToken(r rune) bool { | ||||
| 	return !httpguts.IsTokenRune(r) | ||||
| } | ||||
|  | ||||
| func NewValidator() *validator.Validate { | ||||
| 	validation := validator.New() | ||||
| 	validation.RegisterTagNameFunc(func(fld reflect.StructField) string { | ||||
| 		if fld.Tag.Get("validation_name") != "" { | ||||
| 			return fld.Tag.Get("validation_name") | ||||
| 		} else { | ||||
| 			return fld.Tag.Get("json") | ||||
| 		} | ||||
| 	}) | ||||
| 	validation.RegisterValidation( | ||||
| 		"http_method", | ||||
| 		func(fl validator.FieldLevel) bool { | ||||
| 			method := fl.Field().String() | ||||
| 			// net/http/request.go/validMethod | ||||
| 			return len(method) > 0 && strings.IndexFunc(method, isNotToken) == -1 | ||||
| 		}, | ||||
| 	) | ||||
| 	validation.RegisterValidation( | ||||
| 		"string_bool", | ||||
| 		func(fl validator.FieldLevel) bool { | ||||
| 			s := fl.Field().String() | ||||
| 			return s == "true" || s == "false" || s == "" | ||||
| 		}, | ||||
| 	) | ||||
| 	validation.RegisterValidation( | ||||
| 		"proxy_url", | ||||
| 		func(fl validator.FieldLevel) bool { | ||||
| 			url := fl.Field().String() | ||||
| 			if url == "" { | ||||
| 				return false | ||||
| 			} | ||||
| 			if err := validation.Var(url, "url"); err != nil { | ||||
| 				return false | ||||
| 			} | ||||
| 			if !(url[:7] == "http://" || | ||||
| 				url[:9] == "socks5://" || | ||||
| 				url[:10] == "socks5h://") { | ||||
| 				return false | ||||
| 			} | ||||
| 			return true | ||||
| 		}, | ||||
| 	) | ||||
| 	return validation | ||||
| } | ||||
		Reference in New Issue
	
	Block a user