mirror of
				https://github.com/aykhans/dodo.git
				synced 2025-10-28 11:19:20 +00:00 
			
		
		
		
	Compare commits
	
		
			73 Commits
		
	
	
		
			v0.5.4
			...
			3cd72855e5
		
	
	| Author | SHA1 | Date | |
|---|---|---|---|
| 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 | ||
| 779d5e9b18 | |||
| 6a79d0b1d7 | |||
| 1e53b8a7fb | |||
| 0a8dbec739 | |||
| 3762890914 | |||
| ca6b3d4eb2 | |||
| 1ee06aacc3 | |||
| 3d5834a6a6 | |||
| f1521cbb74 | |||
| 40f8a1cc37 | |||
| 8a3574cd48 | |||
| 5a2a4c47b2 | |||
| ff09a3365e | |||
| c83246abe4 | |||
| efc4c62001 | |||
| ![dependabot[bot]](/assets/img/avatar_default.png)  | f567774e3a | ||
| d2bd60e3ff | |||
| 0fe782c768 | |||
| b9dd14a588 | |||
| 18b1c7dae3 | 
| @@ -4,6 +4,7 @@ binaries | |||||||
| dodo | dodo | ||||||
| .git | .git | ||||||
| .gitignore | .gitignore | ||||||
|  | .golangci.yml | ||||||
| README.md | README.md | ||||||
| LICENSE | LICENSE | ||||||
| config.json | 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,6 +1,6 @@ | |||||||
| FROM golang:1.23.2-alpine AS builder | FROM golang:1.24-alpine AS builder | ||||||
|  |  | ||||||
| WORKDIR /dodo | WORKDIR /src | ||||||
|  |  | ||||||
| COPY go.mod go.sum ./ | COPY go.mod go.sum ./ | ||||||
| RUN go mod download | RUN go mod download | ||||||
| @@ -11,9 +11,9 @@ RUN echo "{}" > config.json | |||||||
|  |  | ||||||
| FROM gcr.io/distroless/static-debian12:latest | FROM gcr.io/distroless/static-debian12:latest | ||||||
|  |  | ||||||
| WORKDIR /dodo | WORKDIR / | ||||||
|  |  | ||||||
| COPY --from=builder /dodo/dodo /dodo/dodo | COPY --from=builder /src/dodo /dodo | ||||||
| COPY --from=builder /dodo/config.json /dodo/config.json | COPY --from=builder /src/config.json /config.json | ||||||
|  |  | ||||||
| ENTRYPOINT ["./dodo", "-c", "/dodo/config.json"] | ENTRYPOINT ["./dodo", "-f", "/config.json"] | ||||||
							
								
								
									
										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 | ||||||
							
								
								
									
										179
									
								
								README.md
									
									
									
									
									
								
							
							
						
						
									
										179
									
								
								README.md
									
									
									
									
									
								
							| @@ -1,22 +1,38 @@ | |||||||
| <h1 align="center">Dodo is a simple and easy-to-use HTTP benchmarking tool.</h1> | <h1 align="center">Dodo - A Fast and Easy-to-Use HTTP Benchmarking Tool</h1> | ||||||
| <p align="center"> | <p align="center"> | ||||||
| <img width="30%" height="30%" src="https://ftp.aykhans.me/web/client/pubshares/hB6VSdCnBCr8gFPeiMuCji/browse?path=%2Fdodo.png"> | <img width="30%" height="30%" src="https://ftp.aykhans.me/web/client/pubshares/VzPtSHS7yPQT7ngoZzZSNU/browse?path=%2Fdodo.png"> | ||||||
| </p> | </p> | ||||||
|  |  | ||||||
| ## Installation | ## Installation | ||||||
| ### With Docker (Recommended) |  | ||||||
|  | ### Using Docker (Recommended) | ||||||
|  |  | ||||||
| Pull the Dodo image from Docker Hub: | Pull the Dodo image from Docker Hub: | ||||||
|  |  | ||||||
| ```sh | ```sh | ||||||
| docker pull aykhans/dodo:latest | 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 | When using Dodo with Docker and a local config file, you must provide the config.json file as a volume to the Docker run command (not as the "-f config.json" argument): | ||||||
| You can grab binaries in the [releases](https://github.com/aykhans/dodo/releases) section. |  | ||||||
|  |  | ||||||
| ### Build from Source | ```sh | ||||||
| To build Dodo from source, you need to have [Go1.22+](https://golang.org/dl/) installed. <br> | docker run -v /path/to/config.json:/config.json aykhans/dodo | ||||||
| Follow the steps below to build dodo: | ``` | ||||||
|  |  | ||||||
|  | If you're using Dodo with Docker and providing a config file via URL, you don't need to set a volume: | ||||||
|  |  | ||||||
|  | ```sh | ||||||
|  | docker run aykhans/dodo -f https://raw.githubusercontent.com/aykhans/dodo/main/config.json | ||||||
|  | ``` | ||||||
|  |  | ||||||
|  | ### Using Binary Files | ||||||
|  |  | ||||||
|  | You can download pre-built binaries from the [releases](https://github.com/aykhans/dodo/releases) section. | ||||||
|  |  | ||||||
|  | ### Building from Source | ||||||
|  |  | ||||||
|  | To build Dodo from source, you need to have [Go 1.24+](https://golang.org/dl/) installed. | ||||||
|  | Follow these steps: | ||||||
|  |  | ||||||
| 1. **Clone the repository:** | 1. **Clone the repository:** | ||||||
|  |  | ||||||
| @@ -39,81 +55,132 @@ Follow the steps below to build dodo: | |||||||
| This will generate an executable named `dodo` in the project directory. | This will generate an executable named `dodo` in the project directory. | ||||||
|  |  | ||||||
| ## Usage | ## 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. |  | ||||||
|  | You can use Dodo with CLI arguments, a JSON config file, or both. When using both, CLI arguments will override JSON config values if there's a conflict. | ||||||
|  |  | ||||||
| ### 1. CLI | ### 1. CLI | ||||||
| Send 1000 GET requests to https://example.com with 10 parallel dodos (threads) and a timeout of 2000 milliseconds: |  | ||||||
|  | Send 1000 GET requests to https://example.com with 10 parallel dodos (threads) and a timeout of 2 seconds: | ||||||
|  |  | ||||||
| ```sh | ```sh | ||||||
| dodo -u https://example.com -m GET -d 10 -r 1000 -t 2000 | dodo -u https://example.com -m GET -d 10 -r 1000 -t 2s | ||||||
| ``` | ``` | ||||||
|  |  | ||||||
| With Docker: | With Docker: | ||||||
|  |  | ||||||
| ```sh | ```sh | ||||||
| docker run --rm -i aykhans/dodo -u https://example.com -m GET -d 10 -r 1000 -t 2000 | docker run --rm -i aykhans/dodo -u https://example.com -m GET -d 10 -r 1000 -t 2s | ||||||
| ``` | ``` | ||||||
|  |  | ||||||
| ### 2. JSON config file | ### 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 | Send 1000 GET requests to https://example.com with 10 parallel dodos (threads) and a timeout of 800 milliseconds: | ||||||
|  |  | ||||||
|  | ```jsonc | ||||||
| { | { | ||||||
|     "method": "GET", |     "method": "GET", | ||||||
|     "url": "https://example.com", |     "url": "https://example.com", | ||||||
|     "no_proxy_check": false, |     "yes": false, | ||||||
|     "timeout": 10000, |     "timeout": "800ms", | ||||||
|     "dodos_count": 1, |     "dodos": 10, | ||||||
|     "request_count": 1, |     "requests": 1000, | ||||||
|     "params": {}, |  | ||||||
|     "headers": {}, |     "params": [ | ||||||
|     "cookies": {}, |         // A random value will be selected from the list for first "key1" param on each request | ||||||
|     "body": [], |         // And always "value" for second "key1" param on each request | ||||||
|     "proxies": [ |         // e.g. "?key1=value2&key1=value" | ||||||
|         { |         { "key1": ["value1", "value2", "value3", "value4"] }, | ||||||
|             "url": "http://example.com:8080", |         { "key1": "value" }, | ||||||
|             "username": "username", |  | ||||||
|             "password": "password" |         // A random value will be selected from the list for param "key2" on each request | ||||||
|         }, |         // e.g. "?key2=value2" | ||||||
|         { |         { "key2": ["value1", "value2"] }, | ||||||
|             "url": "http://example.com:8080" |     ], | ||||||
|         } |  | ||||||
|     ] |     "headers": [ | ||||||
|  |         // A random value will be selected from the list for first "key1" header on each request | ||||||
|  |         // And always "value" for second "key1" header on each request | ||||||
|  |         // e.g. "key1: value3", "key1: value" | ||||||
|  |         { "key1": ["value1", "value2", "value3", "value4"] }, | ||||||
|  |         { "key1": "value" }, | ||||||
|  |  | ||||||
|  |         // A random value will be selected from the list for header "key2" on each request | ||||||
|  |         // e.g. "key2: value2" | ||||||
|  |         { "key2": ["value1", "value2"] }, | ||||||
|  |     ], | ||||||
|  |  | ||||||
|  |     "cookies": [ | ||||||
|  |         // A random value will be selected from the list for first "key1" cookie on each request | ||||||
|  |         // And always "value" for second "key1" cookie on each request | ||||||
|  |         // e.g. "key1=value4; key1=value" | ||||||
|  |         { "key1": ["value1", "value2", "value3", "value4"] }, | ||||||
|  |         { "key1": "value" }, | ||||||
|  |  | ||||||
|  |         // A random value will be selected from the list for cookie "key2" on each request | ||||||
|  |         // e.g. "key2=value1" | ||||||
|  |         { "key2": ["value1", "value2"] }, | ||||||
|  |     ], | ||||||
|  |  | ||||||
|  |     "body": "body-text", | ||||||
|  |     // OR | ||||||
|  |     // A random body value will be selected from the list for each request | ||||||
|  |     "body": ["body-text1", "body-text2", "body-text3"], | ||||||
|  |  | ||||||
|  |     "proxy": "http://example.com:8080", | ||||||
|  |     // OR | ||||||
|  |     // A random proxy will be selected from the list for each request | ||||||
|  |     "proxy": [ | ||||||
|  |         "http://example.com:8080", | ||||||
|  |         "http://username:password@example.com:8080", | ||||||
|  |         "socks5://example.com:8080", | ||||||
|  |         "socks5h://example.com:8080", | ||||||
|  |     ], | ||||||
| } | } | ||||||
| ``` | ``` | ||||||
| Send 1000 GET requests to https://example.com with 10 parallel dodos (threads) and a timeout of 2000 milliseconds: |  | ||||||
|  |  | ||||||
| ```sh | ```sh | ||||||
| dodo -c /path/config.json | dodo -f /path/config.json | ||||||
|  | # OR | ||||||
|  | dodo -f https://example.com/config.json | ||||||
| ``` | ``` | ||||||
|  |  | ||||||
| With Docker: | With Docker: | ||||||
|  |  | ||||||
| ```sh | ```sh | ||||||
| docker run --rm -i -v ./path/config.json:/dodo/config.json aykhans/dodo | docker run --rm -i -v /path/to/config.json:/config.json aykhans/dodo | ||||||
|  | # OR | ||||||
|  | docker run --rm -i aykhans/dodo -f https://example.com/config.json | ||||||
| ``` | ``` | ||||||
|  |  | ||||||
| ### 3. Both (CLI & JSON) | ### 3. Combined (CLI & JSON) | ||||||
|  |  | ||||||
| Override the config file arguments with CLI arguments: | Override the config file arguments with CLI arguments: | ||||||
|  |  | ||||||
| ```sh | ```sh | ||||||
| dodo -c /path/config.json -u https://example.com -m GET -d 10 -r 1000 -t 2000 | dodo -f /path/to/config.json -u https://example.com -m GET -d 10 -r 1000 -t 5s | ||||||
| ``` | ``` | ||||||
|  |  | ||||||
| With Docker: | With Docker: | ||||||
|  |  | ||||||
| ```sh | ```sh | ||||||
| docker run --rm -i -v ./path/config.json:/dodo/config.json aykhans/dodo -u https://example.com -m GET -d 10 -r 1000 -t 2000 | docker run --rm -i -v /path/to/config.json:/config.json aykhans/dodo -u https://example.com -m GET -d 10 -r 1000 -t 5s | ||||||
| ``` | ``` | ||||||
|  |  | ||||||
| ## CLI and JSON Config Parameters | ## 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     | | If `Headers`, `Params`, `Cookies`, `Body`, or `Proxy` fields have multiple values, each request will choose a random value from the list. | ||||||
| | --------------------- | ---------------- | --------------- | -------------- | -------------------------------- | ------------------------------------------------------------------- | ----------- | |  | ||||||
| | Config file           | -                | --config-file   | -c             | String                           | Path to the JSON config file                                        | -           | | | Parameter       | JSON config file | CLI Flag     | CLI Short Flag | Type                           | Description                                                     | Default | | ||||||
| | Yes                   | -                | --yes           | -y             | Boolean                          | Answer yes to all questions                                         | false       | | | --------------- | ---------------- | ------------ | -------------- | ------------------------------ | --------------------------------------------------------------- | ------- | | ||||||
| | URL                   | url              | --url           | -u             | String                           | URL to send the request to                                          | -           | | | Config file     | -                | -config-file | -f             | String                         | Path to local config file or http(s) URL of the config file     | -       | | ||||||
| | Method                | method           | --method        | -m             | String                           | HTTP method                                                         | GET         | | | Yes             | yes              | -yes         | -y             | Boolean                        | Answer yes to all questions                                     | false   | | ||||||
| | Request count         | request_count    | --request-count | -r             | Integer                          | Total number of requests to send                                    | 1000        | | | URL             | url              | -url         | -u             | String                         | URL to send the request to                                      | -       | | ||||||
| | Dodos count (Threads) | dodos_count      | --dodos-count   | -d             | Integer                          | Number of dodos (threads) to send requests in parallel              | 1           | | | Method          | method           | -method      | -m             | String                         | HTTP method                                                     | GET     | | ||||||
| | Timeout               | timeout          | --timeout       | -t             | Integer                          | Timeout for canceling each request (milliseconds)                   | 10000       | | | Requests        | requests         | -requests    | -r             | UnsignedInteger                | Total number of requests to send                                | 1000    | | ||||||
| | No Proxy Check        | no_proxy_check   | --no-proxy-check| -              | Boolean                          | Disable proxy check                                                 | false       | | | Dodos (Threads) | dodos            | -dodos       | -d             | UnsignedInteger                | Number of dodos (threads) to send requests in parallel          | 1       | | ||||||
| | Params                | params           | -               | -              | Key-Value {String: [String]}     | Request parameters                                                  | -           | | | Timeout         | timeout          | -timeout     | -t             | Duration                       | Timeout for canceling each request                              | 10s     | | ||||||
| | Headers               | headers          | -               | -              | Key-Value {String: [String]}     | Request headers                                                     | -           | | | Params          | params           | -param       | -p             | [{String: String OR [String]}] | Request parameters                                              | -       | | ||||||
| | Cookies               | cookies          | -               | -              | Key-Value {String: [String]}     | Request cookies                                                     | -           | | | Headers         | headers          | -header      | -H             | [{String: String OR [String]}] | Request headers                                                 | -       | | ||||||
| | Body                  | body             | -               | -              | [String]                         | Request body                                                        | -           | | | Cookies         | cookies          | -cookie      | -c             | [{String: String OR [String]}] | Request cookies                                                 | -       | | ||||||
| | Proxy                 | proxies          | -               | -              | List[Key-Value {string: string}] | List of proxies (will check active proxies before sending requests) | -           | | | 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                                 | -       | | ||||||
|   | |||||||
							
								
								
									
										47
									
								
								config.json
									
									
									
									
									
								
							
							
						
						
									
										47
									
								
								config.json
									
									
									
									
									
								
							| @@ -1,22 +1,35 @@ | |||||||
| { | { | ||||||
|     "method": "GET", |     "method": "GET", | ||||||
|     "url": "https://example.com", |     "url": "https://example.com", | ||||||
|     "no_proxy_check": false, |     "yes": false, | ||||||
|     "timeout": 10000, |     "timeout": "5s", | ||||||
|     "dodos_count": 1, |     "dodos": 8, | ||||||
|     "request_count": 1, |     "requests": 1000, | ||||||
|     "params": {}, |  | ||||||
|     "headers": {}, |     "params": [ | ||||||
|     "cookies": {}, |         { "key1": ["value1", "value2", "value3", "value4"] }, | ||||||
|     "body": [], |         { "key1": "value" }, | ||||||
|     "proxies": [ |         { "key2": ["value1", "value2"] } | ||||||
|         { |     ], | ||||||
|             "url": "http://example.com:8080", |  | ||||||
|             "username": "username", |     "headers": [ | ||||||
|             "password": "password" |         { "key1": ["value1", "value2", "value3", "value4"] }, | ||||||
|         }, |         { "key1": "value" }, | ||||||
|         { |         { "key2": ["value1", "value2"] } | ||||||
|             "url": "http://example.com:8080" |     ], | ||||||
|         } |  | ||||||
|  |     "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 @@ | |||||||
|  | # YAML/YML config file option is not implemented yet. | ||||||
|  | # This file is a example for future implementation. | ||||||
|  |  | ||||||
|  | method: "GET" | ||||||
|  | url: "https://example.com" | ||||||
|  | yes: false | ||||||
|  | timeout: "5s" | ||||||
|  | dodos: 10 | ||||||
|  | requests: 1000 | ||||||
|  |  | ||||||
|  | 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 | ||||||
|  | body: | ||||||
|  |     - "body-text1" | ||||||
|  |     - "body-text2" | ||||||
|  |     - "body-text3" | ||||||
|  |  | ||||||
|  | # proxy: "http://example.com:8080" | ||||||
|  | # OR | ||||||
|  | proxy: | ||||||
|  |     - "http://example.com:8080" | ||||||
|  |     - "http://username:password@example.com:8080" | ||||||
|  |     - "socks5://example.com:8080" | ||||||
|  |     - "socks5h://example.com:8080" | ||||||
							
								
								
									
										175
									
								
								config/cli.go
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										175
									
								
								config/cli.go
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,175 @@ | |||||||
|  | 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 only with URL: | ||||||
|  |   dodo -u https://example.com | ||||||
|  |  | ||||||
|  | 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 -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 (default %d) | ||||||
|  |   -t, -timeout      Duration  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, | ||||||
|  | 			DefaultRequestCount, | ||||||
|  | 			DefaultTimeout, | ||||||
|  | 			DefaultMethod, | ||||||
|  | 		) | ||||||
|  | 	} | ||||||
|  |  | ||||||
|  | 	var ( | ||||||
|  | 		version      = false | ||||||
|  | 		configFile   = "" | ||||||
|  | 		yes          = false | ||||||
|  | 		method       = "" | ||||||
|  | 		url          types.RequestURL | ||||||
|  | 		dodosCount   = uint(0) | ||||||
|  | 		requestCount = uint(0) | ||||||
|  | 		timeout      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(&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 "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" | ||||||
|  | } | ||||||
							
								
								
									
										344
									
								
								config/config.go
									
									
									
									
									
								
							
							
						
						
									
										344
									
								
								config/config.go
									
									
									
									
									
								
							| @@ -1,195 +1,205 @@ | |||||||
| package config | package config | ||||||
|  |  | ||||||
| import ( | import ( | ||||||
|  | 	"errors" | ||||||
| 	"fmt" | 	"fmt" | ||||||
| 	"net/url" | 	"net/url" | ||||||
| 	"os" | 	"os" | ||||||
|  | 	"slices" | ||||||
|  | 	"strings" | ||||||
| 	"time" | 	"time" | ||||||
|  |  | ||||||
| 	. "github.com/aykhans/dodo/types" | 	"github.com/aykhans/dodo/types" | ||||||
| 	"github.com/aykhans/dodo/utils" | 	"github.com/aykhans/dodo/utils" | ||||||
| 	"github.com/jedib0t/go-pretty/v6/table" | 	"github.com/jedib0t/go-pretty/v6/table" | ||||||
| ) | ) | ||||||
|  |  | ||||||
| const ( | const ( | ||||||
| 	VERSION                 string = "0.5.4" | 	VERSION             string        = "0.6.0" | ||||||
| 	DefaultUserAgent        string = "Dodo/" + VERSION | 	DefaultUserAgent    string        = "Dodo/" + VERSION | ||||||
| 	ProxyCheckURL           string = "https://www.google.com" | 	DefaultMethod       string        = "GET" | ||||||
| 	DefaultMethod           string = "GET" | 	DefaultTimeout      time.Duration = time.Second * 10 | ||||||
| 	DefaultTimeout          uint32 = 10000 // Milliseconds (10 seconds) | 	DefaultDodosCount   uint          = 1 | ||||||
| 	DefaultDodosCount       uint   = 1 | 	DefaultRequestCount uint          = 1 | ||||||
| 	DefaultRequestCount     uint   = 1 | 	DefaultYes          bool          = false | ||||||
| 	MaxDodosCountForProxies uint   = 20 // Max dodos count for proxy check |  | ||||||
| ) | ) | ||||||
|  |  | ||||||
|  | var SupportedProxySchemes []string = []string{"http", "socks5", "socks5h"} | ||||||
|  |  | ||||||
| type RequestConfig struct { | type RequestConfig struct { | ||||||
| 	Method       string | 	Method       string        `json:"method"` | ||||||
| 	URL          *url.URL | 	URL          url.URL       `json:"url"` | ||||||
| 	Timeout      time.Duration | 	Timeout      time.Duration `json:"timeout"` | ||||||
| 	DodosCount   uint | 	DodosCount   uint          `json:"dodos"` | ||||||
| 	RequestCount uint | 	RequestCount uint          `json:"requests"` | ||||||
| 	Params       map[string][]string | 	Yes          bool          `json:"yes"` | ||||||
| 	Headers      map[string][]string | 	Params       types.Params  `json:"params"` | ||||||
| 	Cookies      map[string][]string | 	Headers      types.Headers `json:"headers"` | ||||||
| 	Proxies      []Proxy | 	Cookies      types.Cookies `json:"cookies"` | ||||||
| 	Body         []string | 	Body         types.Body    `json:"body"` | ||||||
| 	Yes          bool | 	Proxies      types.Proxies `json:"proxies"` | ||||||
| 	NoProxyCheck bool |  | ||||||
| } | } | ||||||
|  |  | ||||||
| func (config *RequestConfig) Print() { | func NewRequestConfig(conf *Config) *RequestConfig { | ||||||
| 	t := table.NewWriter() | 	return &RequestConfig{ | ||||||
| 	t.SetOutputMirror(os.Stdout) | 		Method:       *conf.Method, | ||||||
| 	t.SetStyle(table.StyleLight) | 		URL:          conf.URL.URL, | ||||||
| 	t.SetColumnConfigs([]table.ColumnConfig{ | 		Timeout:      conf.Timeout.Duration, | ||||||
| 		{Number: 2, WidthMax: 50}, | 		DodosCount:   *conf.DodosCount, | ||||||
| 	}) | 		RequestCount: *conf.RequestCount, | ||||||
|  | 		Yes:          *conf.Yes, | ||||||
| 	newHeaders := make(map[string][]string) | 		Params:       conf.Params, | ||||||
| 	newHeaders["User-Agent"] = []string{DefaultUserAgent} | 		Headers:      conf.Headers, | ||||||
| 	for k, v := range config.Headers { | 		Cookies:      conf.Cookies, | ||||||
| 		newHeaders[k] = v | 		Body:         conf.Body, | ||||||
|  | 		Proxies:      conf.Proxies, | ||||||
| 	} | 	} | ||||||
|  |  | ||||||
| 	t.AppendHeader(table.Row{"Request Configuration"}) |  | ||||||
| 	t.AppendRow(table.Row{"Method", config.Method}) |  | ||||||
| 	t.AppendSeparator() |  | ||||||
| 	t.AppendRow(table.Row{"URL", config.URL}) |  | ||||||
| 	t.AppendSeparator() |  | ||||||
| 	t.AppendRow(table.Row{"Timeout", fmt.Sprintf("%dms", config.Timeout/time.Millisecond)}) |  | ||||||
| 	t.AppendSeparator() |  | ||||||
| 	t.AppendRow(table.Row{"Dodos", config.DodosCount}) |  | ||||||
| 	t.AppendSeparator() |  | ||||||
| 	t.AppendRow(table.Row{"Requests", config.RequestCount}) |  | ||||||
| 	t.AppendSeparator() |  | ||||||
| 	t.AppendRow(table.Row{"Params", utils.MarshalJSON(config.Params, 3)}) |  | ||||||
| 	t.AppendSeparator() |  | ||||||
| 	t.AppendRow(table.Row{"Headers", utils.MarshalJSON(newHeaders, 3)}) |  | ||||||
| 	t.AppendSeparator() |  | ||||||
| 	t.AppendRow(table.Row{"Cookies", utils.MarshalJSON(config.Cookies, 3)}) |  | ||||||
| 	t.AppendSeparator() |  | ||||||
| 	t.AppendRow(table.Row{"Proxies Count", len(config.Proxies)}) |  | ||||||
| 	t.AppendSeparator() |  | ||||||
| 	t.AppendRow(table.Row{"Proxy Check", !config.NoProxyCheck}) |  | ||||||
| 	t.AppendSeparator() |  | ||||||
| 	t.AppendRow(table.Row{"Body", utils.MarshalJSON(config.Body, 3)}) |  | ||||||
|  |  | ||||||
| 	t.Render() |  | ||||||
| } | } | ||||||
|  |  | ||||||
| func (config *RequestConfig) GetValidDodosCountForRequests() uint { | func (rc *RequestConfig) GetValidDodosCountForRequests() uint { | ||||||
| 	return min(config.DodosCount, config.RequestCount) | 	return min(rc.DodosCount, rc.RequestCount) | ||||||
| } | } | ||||||
|  |  | ||||||
| func (config *RequestConfig) GetValidDodosCountForProxies() uint { | func (rc *RequestConfig) GetMaxConns(minConns uint) uint { | ||||||
| 	return min(config.DodosCount, uint(len(config.Proxies)), MaxDodosCountForProxies) |  | ||||||
| } |  | ||||||
|  |  | ||||||
| func (config *RequestConfig) GetMaxConns(minConns uint) uint { |  | ||||||
| 	maxConns := max( | 	maxConns := max( | ||||||
| 		minConns, uint(config.GetValidDodosCountForRequests()), | 		minConns, rc.GetValidDodosCountForRequests(), | ||||||
| 	) | 	) | ||||||
| 	return ((maxConns * 50 / 100) + maxConns) | 	return ((maxConns * 50 / 100) + maxConns) | ||||||
| } | } | ||||||
|  |  | ||||||
|  | func (rc *RequestConfig) Print() { | ||||||
|  | 	t := table.NewWriter() | ||||||
|  | 	t.SetOutputMirror(os.Stdout) | ||||||
|  | 	t.SetStyle(table.StyleLight) | ||||||
|  | 	t.SetColumnConfigs([]table.ColumnConfig{ | ||||||
|  | 		{ | ||||||
|  | 			Number: 2, | ||||||
|  | 			WidthMaxEnforcer: func(col string, maxLen int) string { | ||||||
|  | 				lines := strings.Split(col, "\n") | ||||||
|  | 				for i, line := range lines { | ||||||
|  | 					if len(line) > maxLen { | ||||||
|  | 						lines[i] = line[:maxLen-3] + "..." | ||||||
|  | 					} | ||||||
|  | 				} | ||||||
|  | 				return strings.Join(lines, "\n") | ||||||
|  | 			}, | ||||||
|  | 			WidthMax: 50}, | ||||||
|  | 	}) | ||||||
|  |  | ||||||
|  | 	t.AppendHeader(table.Row{"Request Configuration"}) | ||||||
|  | 	t.AppendRow(table.Row{"URL", rc.URL.String()}) | ||||||
|  | 	t.AppendSeparator() | ||||||
|  | 	t.AppendRow(table.Row{"Method", rc.Method}) | ||||||
|  | 	t.AppendSeparator() | ||||||
|  | 	t.AppendRow(table.Row{"Timeout", rc.Timeout}) | ||||||
|  | 	t.AppendSeparator() | ||||||
|  | 	t.AppendRow(table.Row{"Dodos", rc.DodosCount}) | ||||||
|  | 	t.AppendSeparator() | ||||||
|  | 	t.AppendRow(table.Row{"Requests", rc.RequestCount}) | ||||||
|  | 	t.AppendSeparator() | ||||||
|  | 	t.AppendRow(table.Row{"Params", rc.Params.String()}) | ||||||
|  | 	t.AppendSeparator() | ||||||
|  | 	t.AppendRow(table.Row{"Headers", rc.Headers.String()}) | ||||||
|  | 	t.AppendSeparator() | ||||||
|  | 	t.AppendRow(table.Row{"Cookies", rc.Cookies.String()}) | ||||||
|  | 	t.AppendSeparator() | ||||||
|  | 	t.AppendRow(table.Row{"Proxy", rc.Proxies.String()}) | ||||||
|  | 	t.AppendSeparator() | ||||||
|  | 	t.AppendRow(table.Row{"Body", rc.Body.String()}) | ||||||
|  |  | ||||||
|  | 	t.Render() | ||||||
|  | } | ||||||
|  |  | ||||||
| type Config struct { | type Config struct { | ||||||
| 	Method       string       `json:"method" validate:"http_method"` // custom validations: http_method | 	Method       *string           `json:"method"` | ||||||
| 	URL          string       `json:"url" validate:"http_url,required"` | 	URL          *types.RequestURL `json:"url"` | ||||||
| 	Timeout      uint32       `json:"timeout" validate:"gte=1,lte=100000"` | 	Timeout      *types.Timeout    `json:"timeout"` | ||||||
| 	DodosCount   uint         `json:"dodos_count" validate:"gte=1"` | 	DodosCount   *uint             `json:"dodos"` | ||||||
| 	RequestCount uint         `json:"request_count" validation_name:"request-count" validate:"gte=1"` | 	RequestCount *uint             `json:"requests"` | ||||||
| 	NoProxyCheck Option[bool] `json:"no_proxy_check"` | 	Yes          *bool             `json:"yes"` | ||||||
|  | 	Params       types.Params      `json:"params"` | ||||||
|  | 	Headers      types.Headers     `json:"headers"` | ||||||
|  | 	Cookies      types.Cookies     `json:"cookies"` | ||||||
|  | 	Body         types.Body        `json:"body"` | ||||||
|  | 	Proxies      types.Proxies     `json:"proxy"` | ||||||
| } | } | ||||||
|  |  | ||||||
| func NewConfig( | func NewConfig() *Config { | ||||||
| 	method string, | 	return &Config{} | ||||||
| 	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 (config *Config) MergeConfigs(newConfig *Config) { | func (c *Config) Validate() []error { | ||||||
| 	if newConfig.Method != "" { | 	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.RequestCount) { | ||||||
|  | 		errs = append(errs, errors.New("request count must be greater than 0")) | ||||||
|  | 	} | ||||||
|  |  | ||||||
|  | 	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 | 		config.Method = newConfig.Method | ||||||
| 	} | 	} | ||||||
| 	if newConfig.URL != "" { | 	if newConfig.URL != nil { | ||||||
| 		config.URL = newConfig.URL | 		config.URL = newConfig.URL | ||||||
| 	} | 	} | ||||||
| 	if newConfig.Timeout != 0 { | 	if newConfig.Timeout != nil { | ||||||
| 		config.Timeout = newConfig.Timeout | 		config.Timeout = newConfig.Timeout | ||||||
| 	} | 	} | ||||||
| 	if newConfig.DodosCount != 0 { | 	if newConfig.DodosCount != nil { | ||||||
| 		config.DodosCount = newConfig.DodosCount | 		config.DodosCount = newConfig.DodosCount | ||||||
| 	} | 	} | ||||||
| 	if newConfig.RequestCount != 0 { | 	if newConfig.RequestCount != nil { | ||||||
| 		config.RequestCount = newConfig.RequestCount | 		config.RequestCount = newConfig.RequestCount | ||||||
| 	} | 	} | ||||||
| 	if !newConfig.NoProxyCheck.IsNone() { | 	if newConfig.Yes != nil { | ||||||
| 		config.NoProxyCheck = newConfig.NoProxyCheck | 		config.Yes = newConfig.Yes | ||||||
| 	} | 	} | ||||||
| } |  | ||||||
|  |  | ||||||
| func (config *Config) SetDefaults() { |  | ||||||
| 	if config.Method == "" { |  | ||||||
| 		config.Method = DefaultMethod |  | ||||||
| 	} |  | ||||||
| 	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 { | 	if len(newConfig.Params) != 0 { | ||||||
| 		config.Params = newConfig.Params | 		config.Params = newConfig.Params | ||||||
| 	} | 	} | ||||||
| @@ -207,28 +217,20 @@ func (config *JSONConfig) MergeConfigs(newConfig *JSONConfig) { | |||||||
| 	} | 	} | ||||||
| } | } | ||||||
|  |  | ||||||
| type CLIConfig struct { | func (config *Config) SetDefaults() { | ||||||
| 	*Config | 	if config.Method == nil { | ||||||
| 	Yes        Option[bool]   `json:"yes" validate:"omitempty"` | 		config.Method = utils.ToPtr(DefaultMethod) | ||||||
| 	ConfigFile string `validation_name:"config-file" validate:"omitempty,filepath"` | 	} | ||||||
| } | 	if config.Timeout == nil { | ||||||
|  | 		config.Timeout = &types.Timeout{Duration: DefaultTimeout} | ||||||
| func NewCLIConfig( | 	} | ||||||
| 	config *Config, | 	if config.DodosCount == nil { | ||||||
| 	yes Option[bool], | 		config.DodosCount = utils.ToPtr(DefaultDodosCount) | ||||||
| 	configFile string, | 	} | ||||||
| ) *CLIConfig { | 	if config.RequestCount == nil { | ||||||
| 	return &CLIConfig{ | 		config.RequestCount = utils.ToPtr(DefaultRequestCount) | ||||||
| 		config, yes, configFile, | 	} | ||||||
| 	} | 	if config.Yes == nil { | ||||||
| } | 		config.Yes = utils.ToPtr(DefaultYes) | ||||||
|  |  | ||||||
| func (config *CLIConfig) MergeConfigs(newConfig *CLIConfig) { |  | ||||||
| 	config.Config.MergeConfigs(newConfig.Config) |  | ||||||
| 	if newConfig.ConfigFile != "" { |  | ||||||
| 		config.ConfigFile = newConfig.ConfigFile |  | ||||||
| 	} |  | ||||||
| 	if !newConfig.Yes.IsNone() { |  | ||||||
| 		config.Yes = newConfig.Yes |  | ||||||
| 	} | 	} | ||||||
| } | } | ||||||
|   | |||||||
							
								
								
									
										60
									
								
								config/file.go
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										60
									
								
								config/file.go
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,60 @@ | |||||||
|  | package config | ||||||
|  |  | ||||||
|  | import ( | ||||||
|  | 	"encoding/json" | ||||||
|  | 	"errors" | ||||||
|  | 	"fmt" | ||||||
|  | 	"io" | ||||||
|  | 	"net/http" | ||||||
|  | 	"os" | ||||||
|  | 	"time" | ||||||
|  |  | ||||||
|  | 	"github.com/aykhans/dodo/types" | ||||||
|  | ) | ||||||
|  |  | ||||||
|  | func (config *Config) ReadFile(filePath types.ConfigFile) error { | ||||||
|  | 	var ( | ||||||
|  | 		data []byte | ||||||
|  | 		err  error | ||||||
|  | 	) | ||||||
|  |  | ||||||
|  | 	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()) | ||||||
|  | 		} | ||||||
|  | 	} | ||||||
|  |  | ||||||
|  | 	return parseJSONConfig(data, config) | ||||||
|  | } | ||||||
|  |  | ||||||
|  | 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 | ||||||
|  | } | ||||||
| @@ -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" |  | ||||||
| } |  | ||||||
							
								
								
									
										22
									
								
								go.mod
									
									
									
									
									
								
							
							
						
						
									
										22
									
								
								go.mod
									
									
									
									
									
								
							| @@ -1,26 +1,20 @@ | |||||||
| module github.com/aykhans/dodo | module github.com/aykhans/dodo | ||||||
|  |  | ||||||
| go 1.23.2 | go 1.24.0 | ||||||
|  |  | ||||||
| require ( | require ( | ||||||
| 	github.com/go-playground/validator/v10 v10.23.0 | 	github.com/jedib0t/go-pretty/v6 v6.6.7 | ||||||
| 	github.com/jedib0t/go-pretty/v6 v6.6.5 | 	github.com/valyala/fasthttp v1.59.0 | ||||||
| 	github.com/valyala/fasthttp v1.58.0 |  | ||||||
| 	golang.org/x/net v0.32.0 |  | ||||||
| ) | ) | ||||||
|  |  | ||||||
| require ( | require ( | ||||||
| 	github.com/andybalholm/brotli v1.1.1 // indirect | 	github.com/andybalholm/brotli v1.1.1 // indirect | ||||||
| 	github.com/gabriel-vasile/mimetype v1.4.4 // indirect |  | ||||||
| 	github.com/go-playground/locales v0.14.1 // indirect |  | ||||||
| 	github.com/go-playground/universal-translator v0.18.1 // indirect |  | ||||||
| 	github.com/klauspost/compress v1.17.11 // 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.16 // indirect | ||||||
| 	github.com/mattn/go-runewidth v0.0.15 // indirect |  | ||||||
| 	github.com/rivo/uniseg v0.4.7 // indirect | 	github.com/rivo/uniseg v0.4.7 // indirect | ||||||
| 	github.com/valyala/bytebufferpool v1.0.0 // indirect | 	github.com/valyala/bytebufferpool v1.0.0 // indirect | ||||||
| 	golang.org/x/crypto v0.31.0 // indirect | 	golang.org/x/net v0.36.0 // indirect | ||||||
| 	golang.org/x/sys v0.28.0 // indirect | 	golang.org/x/sys v0.30.0 // indirect | ||||||
| 	golang.org/x/term v0.27.0 // indirect | 	golang.org/x/term v0.29.0 // indirect | ||||||
| 	golang.org/x/text v0.21.0 // indirect | 	golang.org/x/text v0.22.0 // indirect | ||||||
| ) | ) | ||||||
|   | |||||||
							
								
								
									
										46
									
								
								go.sum
									
									
									
									
									
								
							
							
						
						
									
										46
									
								
								go.sum
									
									
									
									
									
								
							| @@ -2,46 +2,32 @@ github.com/andybalholm/brotli v1.1.1 h1:PR2pgnyFznKEugtsUo0xLdDop5SKXd5Qf5ysW+7X | |||||||
| github.com/andybalholm/brotli v1.1.1/go.mod h1:05ib4cKhjx3OQYUY22hTVd34Bc8upXjOLL2rKwwZBoA= | 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 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c= | ||||||
| github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= | github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= | ||||||
| github.com/gabriel-vasile/mimetype v1.4.4 h1:QjV6pZ7/XZ7ryI2KuyeEDE8wnh7fHP9YnQy+R0LnH8I= | github.com/jedib0t/go-pretty/v6 v6.6.7 h1:m+LbHpm0aIAPLzLbMfn8dc3Ht8MW7lsSO4MPItz/Uuo= | ||||||
| github.com/gabriel-vasile/mimetype v1.4.4/go.mod h1:JwLei5XPtWdGiMFB5Pjle1oEeoSeEuJfJE+TtfvdB/s= | github.com/jedib0t/go-pretty/v6 v6.6.7/go.mod h1:YwC5CE4fJ1HFUDeivSV1r//AmANFHyqczZk+U6BDALU= | ||||||
| github.com/go-playground/assert/v2 v2.2.0 h1:JvknZsQTYeFEAhQwI4qEt9cyV5ONwRHC+lYKSsYSR8s= |  | ||||||
| github.com/go-playground/assert/v2 v2.2.0/go.mod h1:VDjEfimB/XKnb+ZQfWdccd7VUvScMdVu0Titje2rxJ4= |  | ||||||
| github.com/go-playground/locales v0.14.1 h1:EWaQ/wswjilfKLTECiXz7Rh+3BjFhfDFKv/oXslEjJA= |  | ||||||
| github.com/go-playground/locales v0.14.1/go.mod h1:hxrqLVvrK65+Rwrd5Fc6F2O76J/NuW9t0sjnWqG1slY= |  | ||||||
| github.com/go-playground/universal-translator v0.18.1 h1:Bcnm0ZwsGyWbCzImXv+pAJnYK9S473LQFuzCbDbfSFY= |  | ||||||
| github.com/go-playground/universal-translator v0.18.1/go.mod h1:xekY+UJKNuX9WP91TpwSH2VMlDf28Uj24BCp08ZFTUY= |  | ||||||
| github.com/go-playground/validator/v10 v10.23.0 h1:/PwmTwZhS0dPkav3cdK9kV1FsAmrL8sThn8IHr/sO+o= |  | ||||||
| github.com/go-playground/validator/v10 v10.23.0/go.mod h1:dbuPbCMFw/DrkbEynArYaCwl3amGuJotoKCe95atGMM= |  | ||||||
| github.com/jedib0t/go-pretty/v6 v6.6.5 h1:9PgMJOVBedpgYLI56jQRJYqngxYAAzfEUua+3NgSqAo= |  | ||||||
| github.com/jedib0t/go-pretty/v6 v6.6.5/go.mod h1:Uq/HrbhuFty5WSVNfjpQQe47x16RwVGXIveNGEyGtHs= |  | ||||||
| github.com/klauspost/compress v1.17.11 h1:In6xLpyWOi1+C7tXUUWv2ot1QvBjxevKAaI6IXrJmUc= | github.com/klauspost/compress v1.17.11 h1:In6xLpyWOi1+C7tXUUWv2ot1QvBjxevKAaI6IXrJmUc= | ||||||
| github.com/klauspost/compress v1.17.11/go.mod h1:pMDklpSncoRMuLFrf1W9Ss9KT+0rH90U12bZKk7uwG0= | 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/mattn/go-runewidth v0.0.16 h1:E5ScNMtiwvlvB5paMFdw9p4kSQzbXFikJ5SQO6TULQc= | ||||||
| github.com/leodido/go-urn v1.4.0/go.mod h1:bvxc+MVxLKB4z00jd1z+Dvzr47oO32F/QSNjSBOlFxI= | github.com/mattn/go-runewidth v0.0.16/go.mod h1:Jdepj2loyihRzMpdS35Xk/zdY8IAYHsh153qUoGf23w= | ||||||
| github.com/mattn/go-runewidth v0.0.15 h1:UNAjwbU9l54TA3KzvqLGxwWjHmMgBUVhBiTjelZgg3U= |  | ||||||
| github.com/mattn/go-runewidth v0.0.15/go.mod h1:Jdepj2loyihRzMpdS35Xk/zdY8IAYHsh153qUoGf23w= |  | ||||||
| github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM= | github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM= | ||||||
| github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= | 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.2.0/go.mod h1:J6wj4VEh+S6ZtnVlnTBMWIodfgj8LQOQFoIToxlJtxc= | ||||||
| github.com/rivo/uniseg v0.4.7 h1:WUdvkW8uEhrYfLC4ZzdpI2ztxP1I582+49Oc5Mq64VQ= | github.com/rivo/uniseg v0.4.7 h1:WUdvkW8uEhrYfLC4ZzdpI2ztxP1I582+49Oc5Mq64VQ= | ||||||
| github.com/rivo/uniseg v0.4.7/go.mod h1:FN3SvrM+Zdj16jyLfmOkMNblXMcoc8DfTHruCPUcx88= | 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.10.0 h1:Xv5erBjTwe/5IxqUQTdXv5kgmIvbHo3QQyRwhJsOfJA= | ||||||
| github.com/stretchr/testify v1.8.4/go.mod h1:sz/lmYIOXD/1dqDmKjjqLyZ2RngseejIcXlSw2iwfAo= | github.com/stretchr/testify v1.10.0/go.mod h1:r2ic/lqez/lEtzL7wO/rwa5dbSLXVDPFyf8C91i36aY= | ||||||
| github.com/valyala/bytebufferpool v1.0.0 h1:GqA5TC/0021Y/b9FG4Oi9Mr3q7XYx6KllzawFIhcdPw= | github.com/valyala/bytebufferpool v1.0.0 h1:GqA5TC/0021Y/b9FG4Oi9Mr3q7XYx6KllzawFIhcdPw= | ||||||
| github.com/valyala/bytebufferpool v1.0.0/go.mod h1:6bBcMArwyJ5K/AmCkWv1jt77kVWyCJ6HpOuEn7z0Csc= | github.com/valyala/bytebufferpool v1.0.0/go.mod h1:6bBcMArwyJ5K/AmCkWv1jt77kVWyCJ6HpOuEn7z0Csc= | ||||||
| github.com/valyala/fasthttp v1.58.0 h1:GGB2dWxSbEprU9j0iMJHgdKYJVDyjrOwF9RE59PbRuE= | github.com/valyala/fasthttp v1.59.0 h1:Qu0qYHfXvPk1mSLNqcFtEk6DpxgA26hy6bmydotDpRI= | ||||||
| github.com/valyala/fasthttp v1.58.0/go.mod h1:SYXvHHaFp7QZHGKSHmoMipInhrI5StHrhDTYVEjK/Kw= | 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 h1:YtlWPoRdgMu3NZtP45drfy1GKoojuR7hmRcnhZqKjWU= | ||||||
| github.com/xyproto/randomstring v1.0.5/go.mod h1:rgmS5DeNXLivK7YprL0pY+lTuhNQW3iGxZ18UQApw/E= | github.com/xyproto/randomstring v1.0.5/go.mod h1:rgmS5DeNXLivK7YprL0pY+lTuhNQW3iGxZ18UQApw/E= | ||||||
| golang.org/x/crypto v0.31.0 h1:ihbySMvVjLAeSH1IbfcRTkD/iNscyz8rGzjF/E5hV6U= | golang.org/x/net v0.36.0 h1:vWF2fRbw4qslQsQzgFqZff+BItCvGFQqKzKIzx1rmoA= | ||||||
| golang.org/x/crypto v0.31.0/go.mod h1:kDsLvtWBEx7MV9tJOj9bnXsPbxwJQ6csT/x4KIN4Ssk= | golang.org/x/net v0.36.0/go.mod h1:bFmbeoIPfrw4sMHNhb4J9f6+tPziuGjq7Jk/38fxi1I= | ||||||
| golang.org/x/net v0.32.0 h1:ZqPmj8Kzc+Y6e0+skZsuACbx+wzMgo5MQsJh9Qd6aYI= | golang.org/x/sys v0.30.0 h1:QjkSwP/36a20jFYWkSue1YwXzLmsV5Gfq7Eiy72C1uc= | ||||||
| golang.org/x/net v0.32.0/go.mod h1:CwU0IoeOlnQQWJ6ioyFrfRuomB8GKF6KbYXZVyeXNfs= | golang.org/x/sys v0.30.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA= | ||||||
| golang.org/x/sys v0.28.0 h1:Fksou7UEQUWlKvIdsqzJmUmCX3cZuD2+P3XyyzwMhlA= | golang.org/x/term v0.29.0 h1:L6pJp37ocefwRRtYPKSWOWzOtWSxVajvz2ldH/xi3iU= | ||||||
| golang.org/x/sys v0.28.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA= | golang.org/x/term v0.29.0/go.mod h1:6bl4lRlvVuDgSf3179VpIxBF0o10JUpXWOnI7nErv7s= | ||||||
| golang.org/x/term v0.27.0 h1:WP60Sv1nlK1T6SupCHbXzSaN0b9wUmsPoRS9b61A23Q= | golang.org/x/text v0.22.0 h1:bofq7m3/HAFvbF51jz3Q9wLg3jkvSPuiZu/pD1XwgtM= | ||||||
| golang.org/x/term v0.27.0/go.mod h1:iMsnZpn0cago0GOrHO2+Y7u7JPn5AylBrcoWkElMTSM= | golang.org/x/text v0.22.0/go.mod h1:YRoo4H8PVmsu+E3Ou7cqLVH8oXWIHVoX0jqUWALQhfY= | ||||||
| golang.org/x/text v0.21.0 h1:zyQAAkrwaneQ066sspRyJaG9VNi/YJ1NfzcGB3hZ/qo= |  | ||||||
| golang.org/x/text v0.21.0/go.mod h1:4IBbMaMmOPCJ8SecivzSH54+73PCFmPWxNTLm+vZkEQ= |  | ||||||
| gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA= | gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA= | ||||||
| gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= | gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= | ||||||
|   | |||||||
							
								
								
									
										129
									
								
								main.go
									
									
									
									
									
								
							
							
						
						
									
										129
									
								
								main.go
									
									
									
									
									
								
							| @@ -2,121 +2,68 @@ package main | |||||||
|  |  | ||||||
| import ( | import ( | ||||||
| 	"context" | 	"context" | ||||||
|  | 	"errors" | ||||||
| 	"fmt" | 	"fmt" | ||||||
| 	"net/url" |  | ||||||
| 	"os" | 	"os" | ||||||
| 	"os/signal" | 	"os/signal" | ||||||
| 	"strings" |  | ||||||
| 	"syscall" | 	"syscall" | ||||||
| 	"time" |  | ||||||
|  |  | ||||||
| 	"github.com/aykhans/dodo/config" | 	"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/requests" | ||||||
|  | 	"github.com/aykhans/dodo/types" | ||||||
| 	"github.com/aykhans/dodo/utils" | 	"github.com/aykhans/dodo/utils" | ||||||
| 	"github.com/aykhans/dodo/validation" | 	"github.com/jedib0t/go-pretty/v6/text" | ||||||
| 	goValidator "github.com/go-playground/validator/v10" |  | ||||||
| ) | ) | ||||||
|  |  | ||||||
| func main() { | func main() { | ||||||
| 	validator := validation.NewValidator() | 	conf := config.NewConfig() | ||||||
| 	conf := config.NewConfig("", 0, 0, 0, nil) | 	configFile, err := conf.ReadCLI() | ||||||
| 	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) |  | ||||||
| 	if err != nil { | 	if err != nil { | ||||||
| 		utils.PrintErrAndExit(err) | 		utils.PrintErrAndExit(err) | ||||||
| 	} | 	} | ||||||
| 	requestConf := &config.RequestConfig{ |  | ||||||
| 		Method:       conf.Method, | 	if configFile.String() != "" { | ||||||
| 		URL:          parsedURL, | 		tempConf := config.NewConfig() | ||||||
| 		Timeout:      time.Duration(conf.Timeout) * time.Millisecond, | 		if err := tempConf.ReadFile(configFile); err != nil { | ||||||
| 		DodosCount:   conf.DodosCount, | 			utils.PrintErrAndExit(err) | ||||||
| 		RequestCount: conf.RequestCount, | 		} | ||||||
| 		Params:       jsonConf.Params, | 		tempConf.MergeConfig(conf) | ||||||
| 		Headers:      jsonConf.Headers, | 		conf = tempConf | ||||||
| 		Cookies:      jsonConf.Cookies, | 	} | ||||||
| 		Proxies:      jsonConf.Proxies, | 	conf.SetDefaults() | ||||||
| 		Body:         jsonConf.Body, |  | ||||||
| 		Yes:          cliConf.Yes.ValueOr(false), | 	if errs := conf.Validate(); len(errs) > 0 { | ||||||
| 		NoProxyCheck: conf.NoProxyCheck.ValueOr(false), | 		utils.PrintErrAndExit(errors.Join(errs...)) | ||||||
| 	} | 	} | ||||||
| 	requestConf.Print() |  | ||||||
| 	if !cliConf.Yes.ValueOr(false) { | 	requestConf := config.NewRequestConfig(conf) | ||||||
| 		response := readers.CLIYesOrNoReader("Do you want to continue?", true) | 	requestConf.Print() | ||||||
| 		if !response { |  | ||||||
| 			utils.PrintAndExit("Exiting...") | 	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()) | 	ctx, cancel := context.WithCancel(context.Background()) | ||||||
| 	sigChan := make(chan os.Signal, 1) | 	go listenForTermination(func() { cancel() }) | ||||||
| 	signal.Notify(sigChan, syscall.SIGINT, syscall.SIGTERM) |  | ||||||
| 	go func() { |  | ||||||
| 		<-sigChan |  | ||||||
| 		cancel() |  | ||||||
| 	}() |  | ||||||
|  |  | ||||||
| 	responses, err := requests.Run(ctx, requestConf) | 	responses, err := requests.Run(ctx, requestConf) | ||||||
| 	if err != nil { | 	if err != nil { | ||||||
| 		if customerrors.Is(err, customerrors.ErrInterrupt) { | 		if err == types.ErrInterrupt { | ||||||
| 			utils.PrintlnC(utils.Colors.Yellow, err.Error()) | 			fmt.Println(text.FgYellow.Sprint(err.Error())) | ||||||
| 			return |  | ||||||
| 		} else if customerrors.Is(err, customerrors.ErrNoInternet) { |  | ||||||
| 			utils.PrintAndExit("No internet connection") |  | ||||||
| 			return | 			return | ||||||
| 		} | 		} | ||||||
| 		panic(err) | 		utils.PrintErrAndExit(err) | ||||||
| 	} | 	} | ||||||
|  |  | ||||||
| 	responses.Print() | 	responses.Print() | ||||||
| } | } | ||||||
|  |  | ||||||
|  | func listenForTermination(do func()) { | ||||||
|  | 	sigChan := make(chan os.Signal, 1) | ||||||
|  | 	signal.Notify(sigChan, syscall.SIGINT, syscall.SIGTERM) | ||||||
|  | 	<-sigChan | ||||||
|  | 	do() | ||||||
|  | } | ||||||
|   | |||||||
							
								
								
									
										155
									
								
								readers/cli.go
									
									
									
									
									
								
							
							
						
						
									
										155
									
								
								readers/cli.go
									
									
									
									
									
								
							| @@ -1,155 +0,0 @@ | |||||||
| package readers |  | ||||||
|  |  | ||||||
| import ( |  | ||||||
| 	"flag" |  | ||||||
| 	"fmt" |  | ||||||
| 	"strings" |  | ||||||
|  |  | ||||||
| 	"github.com/aykhans/dodo/config" |  | ||||||
| 	. "github.com/aykhans/dodo/types" |  | ||||||
| 	"github.com/aykhans/dodo/utils" |  | ||||||
| ) |  | ||||||
|  |  | ||||||
| const usageText = `Usage: |  | ||||||
|   dodo [flags] |  | ||||||
|  |  | ||||||
| Examples: |  | ||||||
|  |  | ||||||
| Simple usage only with URL: |  | ||||||
|   dodo -u https://example.com |  | ||||||
|  |  | ||||||
| Simple usage with config file: |  | ||||||
|   dodo -c /path/to/config/file/config.json |  | ||||||
|  |  | ||||||
| Usage with all flags: |  | ||||||
|   dodo -c /path/to/config/file/config.json -u https://example.com -m POST -d 10 -r 1000 -t 2000 --no-proxy-check -y |  | ||||||
|  |  | ||||||
| Flags: |  | ||||||
|   -h, --help                 help for dodo |  | ||||||
|   -v, --version              version for dodo |  | ||||||
|   -c, --config-file string   Path to the config file |  | ||||||
|   -d, --dodos-count uint     Number of dodos(threads) (default %d) |  | ||||||
|   -m, --method string        HTTP Method (default %s) |  | ||||||
|   -r, --request-count uint   Number of total requests (default %d) |  | ||||||
|   -t, --timeout uint32       Timeout for each request in milliseconds (default %d) |  | ||||||
|   -u, --url string           URL for stress testing |  | ||||||
|       --no-proxy-check bool  Do not check for proxies (default false) |  | ||||||
|   -y, --yes bool             Answer yes to all questions (default false)` |  | ||||||
|  |  | ||||||
| func CLIConfigReader() (*config.CLIConfig, error) { |  | ||||||
| 	flag.Usage = func() { |  | ||||||
| 		fmt.Printf( |  | ||||||
| 			usageText+"\n", |  | ||||||
| 			config.DefaultDodosCount, |  | ||||||
| 			config.DefaultMethod, |  | ||||||
| 			config.DefaultRequestCount, |  | ||||||
| 			config.DefaultTimeout, |  | ||||||
| 		) |  | ||||||
| 	} |  | ||||||
|  |  | ||||||
| 	var ( |  | ||||||
| 		cliConfig          = config.NewCLIConfig(config.NewConfig("", 0, 0, 0, nil), NewOption(false), "") |  | ||||||
| 		configFile         = "" |  | ||||||
| 		yes                = false |  | ||||||
| 		method             = "" |  | ||||||
| 		url                = "" |  | ||||||
| 		dodosCount    uint = 0 |  | ||||||
| 		requestsCount uint = 0 |  | ||||||
| 		timeout       uint = 0 |  | ||||||
| 		noProxyCheck  bool = false |  | ||||||
| 	) |  | ||||||
| 	{ |  | ||||||
| 		flag.Bool("version", false, "Prints the version of the program") |  | ||||||
| 		flag.Bool("v", false, "Prints the version of the program") |  | ||||||
|  |  | ||||||
| 		flag.StringVar(&configFile, "config-file", "", "Path to the configuration file") |  | ||||||
| 		flag.StringVar(&configFile, "c", "", "Path to the configuration file") |  | ||||||
|  |  | ||||||
| 		flag.BoolVar(&yes, "yes", false, "Answer yes to all questions") |  | ||||||
| 		flag.BoolVar(&yes, "y", false, "Answer yes to all questions") |  | ||||||
|  |  | ||||||
| 		flag.StringVar(&method, "method", "", "HTTP Method") |  | ||||||
| 		flag.StringVar(&method, "m", "", "HTTP Method") |  | ||||||
|  |  | ||||||
| 		flag.StringVar(&url, "url", "", "URL to send the request") |  | ||||||
| 		flag.StringVar(&url, "u", "", "URL to send the request") |  | ||||||
|  |  | ||||||
| 		flag.UintVar(&dodosCount, "dodos-count", 0, "Number of dodos(threads)") |  | ||||||
| 		flag.UintVar(&dodosCount, "d", 0, "Number of dodos(threads)") |  | ||||||
|  |  | ||||||
| 		flag.UintVar(&requestsCount, "requests-count", 0, "Number of total requests") |  | ||||||
| 		flag.UintVar(&requestsCount, "r", 0, "Number of total requests") |  | ||||||
|  |  | ||||||
| 		flag.UintVar(&timeout, "timeout", 0, "Timeout for each request in milliseconds") |  | ||||||
| 		flag.UintVar(&timeout, "t", 0, "Timeout for each request in milliseconds") |  | ||||||
|  |  | ||||||
| 		flag.BoolVar(&noProxyCheck, "no-proxy-check", false, "Do not check for active proxies") |  | ||||||
| 	} |  | ||||||
|  |  | ||||||
| 	flag.Parse() |  | ||||||
|  |  | ||||||
| 	args := flag.Args() |  | ||||||
| 	if len(args) > 0 { |  | ||||||
| 		return nil, fmt.Errorf("unexpected arguments: %v", strings.Join(args, ", ")) |  | ||||||
| 	} |  | ||||||
|  |  | ||||||
| 	returnNil := false |  | ||||||
| 	flag.Visit(func(f *flag.Flag) { |  | ||||||
| 		switch f.Name { |  | ||||||
| 		case "version", "v": |  | ||||||
| 			fmt.Printf("dodo version %s\n", config.VERSION) |  | ||||||
| 			returnNil = true |  | ||||||
| 		case "config-file", "c": |  | ||||||
| 			cliConfig.ConfigFile = configFile |  | ||||||
| 		case "yes", "y": |  | ||||||
| 			cliConfig.Yes.SetValue(yes) |  | ||||||
| 		case "method", "m": |  | ||||||
| 			cliConfig.Method = method |  | ||||||
| 		case "url", "u": |  | ||||||
| 			cliConfig.URL = url |  | ||||||
| 		case "dodos-count", "d": |  | ||||||
| 			cliConfig.DodosCount = dodosCount |  | ||||||
| 		case "requests-count", "r": |  | ||||||
| 			cliConfig.RequestCount = requestsCount |  | ||||||
| 		case "timeout", "t": |  | ||||||
| 			var maxUint32 uint = 4294967295 |  | ||||||
| 			if timeout > maxUint32 { |  | ||||||
| 				utils.PrintfC(utils.Colors.Yellow, "timeout value is too large, setting to %d\n", maxUint32) |  | ||||||
| 				timeout = maxUint32 |  | ||||||
| 			} |  | ||||||
| 			cliConfig.Timeout = uint32(timeout) |  | ||||||
| 		case "no-proxy-check": |  | ||||||
| 			cliConfig.NoProxyCheck.SetValue(noProxyCheck) |  | ||||||
| 		} |  | ||||||
| 	}) |  | ||||||
|  |  | ||||||
| 	if returnNil { |  | ||||||
| 		return nil, nil |  | ||||||
| 	} |  | ||||||
| 	return cliConfig, nil |  | ||||||
| } |  | ||||||
|  |  | ||||||
| // CLIYesOrNoReader reads a yes or no answer from the command line. |  | ||||||
| // It prompts the user with the given message and default value, |  | ||||||
| // and returns true if the user answers "y" or "Y", and false otherwise. |  | ||||||
| // If there is an error while reading the input, it returns false. |  | ||||||
| // If the user simply presses enter without providing any input, |  | ||||||
| // it returns the default value specified by the `dft` parameter. |  | ||||||
| func CLIYesOrNoReader(message string, dft bool) bool { |  | ||||||
| 	var answer string |  | ||||||
| 	defaultMessage := "Y/n" |  | ||||||
| 	if !dft { |  | ||||||
| 		defaultMessage = "y/N" |  | ||||||
| 	} |  | ||||||
| 	fmt.Printf("%s [%s]: ", message, defaultMessage) |  | ||||||
| 	if _, err := fmt.Scanln(&answer); err != nil { |  | ||||||
| 		if err.Error() == "unexpected newline" { |  | ||||||
| 			return dft |  | ||||||
| 		} |  | ||||||
| 		return false |  | ||||||
| 	} |  | ||||||
| 	if answer == "" { |  | ||||||
| 		return dft |  | ||||||
| 	} |  | ||||||
| 	return answer == "y" || answer == "Y" |  | ||||||
| } |  | ||||||
| @@ -1,36 +0,0 @@ | |||||||
| package readers |  | ||||||
|  |  | ||||||
| import ( |  | ||||||
| 	"encoding/json" |  | ||||||
| 	"os" |  | ||||||
|  |  | ||||||
| 	"github.com/aykhans/dodo/config" |  | ||||||
| 	customerrors "github.com/aykhans/dodo/custom_errors" |  | ||||||
| ) |  | ||||||
|  |  | ||||||
| func JSONConfigReader(filePath string) (*config.JSONConfig, error) { |  | ||||||
| 	data, err := os.ReadFile(filePath) |  | ||||||
| 	if err != nil { |  | ||||||
| 		return nil, customerrors.OSErrorFormater(err) |  | ||||||
| 	} |  | ||||||
| 	jsonConf := config.NewJSONConfig( |  | ||||||
| 		config.NewConfig("", 0, 0, 0, nil), |  | ||||||
| 		nil, nil, nil, nil, nil, |  | ||||||
| 	) |  | ||||||
| 	err = json.Unmarshal(data, &jsonConf) |  | ||||||
|  |  | ||||||
| 	if err != nil { |  | ||||||
| 		switch err := err.(type) { |  | ||||||
| 		case *json.UnmarshalTypeError: |  | ||||||
| 			return nil, |  | ||||||
| 				customerrors.NewTypeError( |  | ||||||
| 					err.Type.String(), |  | ||||||
| 					err.Value, |  | ||||||
| 					err.Field, |  | ||||||
| 					err, |  | ||||||
| 				) |  | ||||||
| 		} |  | ||||||
| 		return nil, customerrors.NewInvalidFileError(filePath, err) |  | ||||||
| 	} |  | ||||||
| 	return jsonConf, nil |  | ||||||
| } |  | ||||||
| @@ -2,14 +2,11 @@ package requests | |||||||
|  |  | ||||||
| import ( | import ( | ||||||
| 	"context" | 	"context" | ||||||
| 	"fmt" | 	"errors" | ||||||
| 	"math/rand" | 	"math/rand" | ||||||
| 	"net/url" | 	"net/url" | ||||||
| 	"sync" |  | ||||||
| 	"time" | 	"time" | ||||||
|  |  | ||||||
| 	"github.com/aykhans/dodo/config" |  | ||||||
| 	"github.com/aykhans/dodo/readers" |  | ||||||
| 	"github.com/aykhans/dodo/utils" | 	"github.com/aykhans/dodo/utils" | ||||||
| 	"github.com/valyala/fasthttp" | 	"github.com/valyala/fasthttp" | ||||||
| 	"github.com/valyala/fasthttp/fasthttpproxy" | 	"github.com/valyala/fasthttp/fasthttpproxy" | ||||||
| @@ -22,80 +19,38 @@ type ClientGeneratorFunc func() *fasthttp.HostClient | |||||||
| func getClients( | func getClients( | ||||||
| 	ctx context.Context, | 	ctx context.Context, | ||||||
| 	timeout time.Duration, | 	timeout time.Duration, | ||||||
| 	proxies []config.Proxy, | 	proxies []url.URL, | ||||||
| 	dodosCount uint, |  | ||||||
| 	maxConns uint, | 	maxConns uint, | ||||||
| 	yes bool, | 	URL url.URL, | ||||||
| 	noProxyCheck bool, |  | ||||||
| 	URL *url.URL, |  | ||||||
| ) []*fasthttp.HostClient { | ) []*fasthttp.HostClient { | ||||||
| 	isTLS := URL.Scheme == "https" | 	isTLS := URL.Scheme == "https" | ||||||
|  |  | ||||||
| 	if proxiesLen := len(proxies); proxiesLen > 0 { | 	if proxiesLen := len(proxies); proxiesLen > 0 { | ||||||
| 		// If noProxyCheck is true, we will return the clients without checking the proxies. | 		clients := make([]*fasthttp.HostClient, 0, proxiesLen) | ||||||
| 		if noProxyCheck { | 		addr := URL.Host | ||||||
| 			clients := make([]*fasthttp.HostClient, 0, proxiesLen) | 		if isTLS && URL.Port() == "" { | ||||||
| 			addr := URL.Host | 			addr += ":443" | ||||||
| 			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 |  | ||||||
| 		} | 		} | ||||||
|  |  | ||||||
| 		// Else, we will check the proxies and return the active ones. | 		for _, proxy := range proxies { | ||||||
| 		activeProxyClients := getActiveProxyClients( | 			dialFunc, err := getDialFunc(&proxy, timeout) | ||||||
| 			ctx, proxies, timeout, dodosCount, maxConns, URL, | 			if err != nil { | ||||||
| 		) | 				continue | ||||||
| 		if ctx.Err() != nil { | 			} | ||||||
| 			return nil |  | ||||||
| 		} | 			clients = append(clients, &fasthttp.HostClient{ | ||||||
| 		activeProxyClientsCount := uint(len(activeProxyClients)) | 				MaxConns:            int(maxConns), | ||||||
| 		var yesOrNoMessage string | 				IsTLS:               isTLS, | ||||||
| 		var yesOrNoDefault bool | 				Addr:                addr, | ||||||
| 		if activeProxyClientsCount == 0 { | 				Dial:                dialFunc, | ||||||
| 			yesOrNoDefault = false | 				MaxIdleConnDuration: timeout, | ||||||
| 			yesOrNoMessage = utils.Colored( | 				MaxConnDuration:     timeout, | ||||||
| 				utils.Colors.Yellow, | 				WriteTimeout:        timeout, | ||||||
| 				"No active proxies found. Do you want to continue?", | 				ReadTimeout:         timeout, | ||||||
| 			) | 			}, | ||||||
| 		} else { |  | ||||||
| 			yesOrNoMessage = utils.Colored( |  | ||||||
| 				utils.Colors.Yellow, |  | ||||||
| 				fmt.Sprintf( |  | ||||||
| 					"Found %d active proxies. Do you want to continue?", |  | ||||||
| 					activeProxyClientsCount, |  | ||||||
| 				), |  | ||||||
| 			) | 			) | ||||||
| 		} | 		} | ||||||
| 		if !yes { | 		return clients | ||||||
| 			response := readers.CLIYesOrNoReader("\n"+yesOrNoMessage, yesOrNoDefault) |  | ||||||
| 			if !response { |  | ||||||
| 				utils.PrintAndExit("Exiting...") |  | ||||||
| 			} |  | ||||||
| 		} |  | ||||||
| 		fmt.Println() |  | ||||||
| 		if activeProxyClientsCount > 0 { |  | ||||||
| 			return activeProxyClients |  | ||||||
| 		} |  | ||||||
| 	} | 	} | ||||||
|  |  | ||||||
| 	client := &fasthttp.HostClient{ | 	client := &fasthttp.HostClient{ | ||||||
| @@ -110,200 +65,19 @@ func getClients( | |||||||
| 	return []*fasthttp.HostClient{client} | 	return []*fasthttp.HostClient{client} | ||||||
| } | } | ||||||
|  |  | ||||||
| // getActiveProxyClients divides the proxies into slices based on the number of dodos and | // getDialFunc returns the appropriate fasthttp.DialFunc based on the provided proxy URL scheme. | ||||||
| // launches goroutines to find active proxy clients for each slice. | // It supports SOCKS5 ('socks5' or 'socks5h') and HTTP ('http') proxy schemes. | ||||||
| // It uses a progress tracker to monitor the progress of the search. | // For HTTP proxies, the timeout parameter determines connection timeouts. | ||||||
| // Once all goroutines have completed, the function waits for them to finish and | // Returns an error if the proxy scheme is unsupported. | ||||||
| // returns a flattened slice of active proxy clients. | func getDialFunc(proxy *url.URL, timeout time.Duration) (fasthttp.DialFunc, error) { | ||||||
| func getActiveProxyClients( |  | ||||||
| 	ctx context.Context, |  | ||||||
| 	proxies []config.Proxy, |  | ||||||
| 	timeout time.Duration, |  | ||||||
| 	dodosCount uint, |  | ||||||
| 	maxConns uint, |  | ||||||
| 	URL *url.URL, |  | ||||||
| ) []*fasthttp.HostClient { |  | ||||||
| 	activeProxyClientsArray := make([][]*fasthttp.HostClient, dodosCount) |  | ||||||
| 	proxiesCount := len(proxies) |  | ||||||
| 	dodosCountInt := int(dodosCount) |  | ||||||
|  |  | ||||||
| 	var ( |  | ||||||
| 		wg       sync.WaitGroup |  | ||||||
| 		streamWG sync.WaitGroup |  | ||||||
| 	) |  | ||||||
| 	wg.Add(dodosCountInt) |  | ||||||
| 	streamWG.Add(1) |  | ||||||
| 	var proxiesSlice []config.Proxy |  | ||||||
| 	increase := make(chan int64, proxiesCount) |  | ||||||
|  |  | ||||||
| 	streamCtx, streamCtxCancel := context.WithCancel(context.Background()) |  | ||||||
| 	go streamProgress(streamCtx, &streamWG, int64(proxiesCount), "Searching for active proxies🌐", increase) |  | ||||||
|  |  | ||||||
| 	for i := range dodosCountInt { |  | ||||||
| 		if i+1 == dodosCountInt { |  | ||||||
| 			proxiesSlice = proxies[i*proxiesCount/dodosCountInt:] |  | ||||||
| 		} else { |  | ||||||
| 			proxiesSlice = proxies[i*proxiesCount/dodosCountInt : (i+1)*proxiesCount/dodosCountInt] |  | ||||||
| 		} |  | ||||||
| 		go findActiveProxyClients( |  | ||||||
| 			ctx, |  | ||||||
| 			proxiesSlice, |  | ||||||
| 			timeout, |  | ||||||
| 			&activeProxyClientsArray[i], |  | ||||||
| 			increase, |  | ||||||
| 			maxConns, |  | ||||||
| 			URL, |  | ||||||
| 			&wg, |  | ||||||
| 		) |  | ||||||
| 	} |  | ||||||
| 	wg.Wait() |  | ||||||
| 	streamCtxCancel() |  | ||||||
| 	streamWG.Wait() |  | ||||||
| 	return utils.Flatten(activeProxyClientsArray) |  | ||||||
| } |  | ||||||
|  |  | ||||||
| // findActiveProxyClients checks a list of proxies to determine which ones are active |  | ||||||
| // and appends the active ones to the provided activeProxyClients slice. |  | ||||||
| // |  | ||||||
| // Parameters: |  | ||||||
| //   - ctx: The context to control cancellation and timeout. |  | ||||||
| //   - proxies: A slice of Proxy configurations to be checked. |  | ||||||
| //   - timeout: The duration to wait for each proxy check before timing out. |  | ||||||
| //   - activeProxyClients: A pointer to a slice where active proxy clients will be appended. |  | ||||||
| //   - increase: A channel to signal the increase of checked proxies count. |  | ||||||
| //   - URL: The URL to be used for checking the proxies. |  | ||||||
| //   - wg: A WaitGroup to signal when the function is done. |  | ||||||
| // |  | ||||||
| // The function sends a GET request to each proxy using the provided URL. If the proxy |  | ||||||
| // responds with a status code of 200, it is considered active and added to the activeProxyClients slice. |  | ||||||
| // The function respects the context's cancellation and timeout settings. |  | ||||||
| func findActiveProxyClients( |  | ||||||
| 	ctx context.Context, |  | ||||||
| 	proxies []config.Proxy, |  | ||||||
| 	timeout time.Duration, |  | ||||||
| 	activeProxyClients *[]*fasthttp.HostClient, |  | ||||||
| 	increase chan<- int64, |  | ||||||
| 	maxConns uint, |  | ||||||
| 	URL *url.URL, |  | ||||||
| 	wg *sync.WaitGroup, |  | ||||||
| ) { |  | ||||||
| 	defer wg.Done() |  | ||||||
|  |  | ||||||
| 	request := fasthttp.AcquireRequest() |  | ||||||
| 	defer fasthttp.ReleaseRequest(request) |  | ||||||
| 	request.SetRequestURI(config.ProxyCheckURL) |  | ||||||
| 	request.Header.SetMethod("GET") |  | ||||||
|  |  | ||||||
| 	for _, proxy := range proxies { |  | ||||||
| 		if ctx.Err() != nil { |  | ||||||
| 			return |  | ||||||
| 		} |  | ||||||
|  |  | ||||||
| 		func() { |  | ||||||
| 			defer func() { increase <- 1 }() |  | ||||||
|  |  | ||||||
| 			response := fasthttp.AcquireResponse() |  | ||||||
| 			defer fasthttp.ReleaseResponse(response) |  | ||||||
|  |  | ||||||
| 			dialFunc, err := getDialFunc(&proxy, timeout) |  | ||||||
| 			if err != nil { |  | ||||||
| 				return |  | ||||||
| 			} |  | ||||||
| 			client := &fasthttp.Client{ |  | ||||||
| 				Dial: dialFunc, |  | ||||||
| 			} |  | ||||||
| 			defer client.CloseIdleConnections() |  | ||||||
|  |  | ||||||
| 			ch := make(chan error) |  | ||||||
| 			go func() { |  | ||||||
| 				err := client.DoTimeout(request, response, timeout) |  | ||||||
| 				ch <- err |  | ||||||
| 			}() |  | ||||||
| 			select { |  | ||||||
| 			case err := <-ch: |  | ||||||
| 				if err != nil { |  | ||||||
| 					return |  | ||||||
| 				} |  | ||||||
| 				break |  | ||||||
| 			case <-time.After(timeout): |  | ||||||
| 				return |  | ||||||
| 			case <-ctx.Done(): |  | ||||||
| 				return |  | ||||||
| 			} |  | ||||||
|  |  | ||||||
| 			isTLS := URL.Scheme == "https" |  | ||||||
| 			addr := URL.Host |  | ||||||
| 			if isTLS && URL.Port() == "" { |  | ||||||
| 				addr += ":443" |  | ||||||
| 			} |  | ||||||
| 			if response.StatusCode() == 200 { |  | ||||||
| 				*activeProxyClients = append( |  | ||||||
| 					*activeProxyClients, |  | ||||||
| 					&fasthttp.HostClient{ |  | ||||||
| 						MaxConns:            int(maxConns), |  | ||||||
| 						IsTLS:               isTLS, |  | ||||||
| 						Addr:                addr, |  | ||||||
| 						Dial:                dialFunc, |  | ||||||
| 						MaxIdleConnDuration: timeout, |  | ||||||
| 						MaxConnDuration:     timeout, |  | ||||||
| 						WriteTimeout:        timeout, |  | ||||||
| 						ReadTimeout:         timeout, |  | ||||||
| 					}, |  | ||||||
| 				) |  | ||||||
| 			} |  | ||||||
| 		}() |  | ||||||
| 	} |  | ||||||
| } |  | ||||||
|  |  | ||||||
| // getDialFunc returns a fasthttp.DialFunc based on the provided proxy configuration. |  | ||||||
| // It takes a pointer to a config.Proxy struct as input and returns a fasthttp.DialFunc and an error. |  | ||||||
| // The function parses the proxy URL, determines the scheme (socks5, socks5h, http, or https), |  | ||||||
| // and creates a dialer accordingly. If the proxy URL is invalid or the scheme is not supported, |  | ||||||
| // it returns an error. |  | ||||||
| func getDialFunc(proxy *config.Proxy, timeout time.Duration) (fasthttp.DialFunc, error) { |  | ||||||
| 	parsedProxyURL, err := url.Parse(proxy.URL) |  | ||||||
| 	if err != nil { |  | ||||||
| 		return nil, err |  | ||||||
| 	} |  | ||||||
|  |  | ||||||
| 	var dialer fasthttp.DialFunc | 	var dialer fasthttp.DialFunc | ||||||
| 	if parsedProxyURL.Scheme == "socks5" || parsedProxyURL.Scheme == "socks5h" { |  | ||||||
| 		if proxy.Username != "" { | 	if proxy.Scheme == "socks5" || proxy.Scheme == "socks5h" { | ||||||
| 			dialer = fasthttpproxy.FasthttpSocksDialer( | 		dialer = fasthttpproxy.FasthttpSocksDialerDualStack(proxy.String()) | ||||||
| 				fmt.Sprintf( | 	} else if proxy.Scheme == "http" { | ||||||
| 					"%s://%s:%s@%s", | 		dialer = fasthttpproxy.FasthttpHTTPDialerDualStackTimeout(proxy.String(), timeout) | ||||||
| 					parsedProxyURL.Scheme, |  | ||||||
| 					proxy.Username, |  | ||||||
| 					proxy.Password, |  | ||||||
| 					parsedProxyURL.Host, |  | ||||||
| 				), |  | ||||||
| 			) |  | ||||||
| 		} else { |  | ||||||
| 			dialer = fasthttpproxy.FasthttpSocksDialer( |  | ||||||
| 				fmt.Sprintf( |  | ||||||
| 					"%s://%s", |  | ||||||
| 					parsedProxyURL.Scheme, |  | ||||||
| 					parsedProxyURL.Host, |  | ||||||
| 				), |  | ||||||
| 			) |  | ||||||
| 		} |  | ||||||
| 	} else if parsedProxyURL.Scheme == "http" { |  | ||||||
| 		if proxy.Username != "" { |  | ||||||
| 			dialer = fasthttpproxy.FasthttpHTTPDialerTimeout( |  | ||||||
| 				fmt.Sprintf( |  | ||||||
| 					"%s:%s@%s", |  | ||||||
| 					proxy.Username, proxy.Password, parsedProxyURL.Host, |  | ||||||
| 				), |  | ||||||
| 				timeout, |  | ||||||
| 			) |  | ||||||
| 		} else { |  | ||||||
| 			dialer = fasthttpproxy.FasthttpHTTPDialerTimeout( |  | ||||||
| 				parsedProxyURL.Host, |  | ||||||
| 				timeout, |  | ||||||
| 			) |  | ||||||
| 		} |  | ||||||
| 	} else { | 	} else { | ||||||
| 		return nil, err | 		return nil, errors.New("unsupported proxy scheme") | ||||||
| 	} | 	} | ||||||
| 	return dialer, nil | 	return dialer, nil | ||||||
| } | } | ||||||
|   | |||||||
| @@ -7,7 +7,6 @@ import ( | |||||||
| 	"time" | 	"time" | ||||||
|  |  | ||||||
| 	"github.com/jedib0t/go-pretty/v6/progress" | 	"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. | // streamProgress streams the progress of a task to the console using a progress bar. | ||||||
| @@ -37,9 +36,11 @@ func streamProgress( | |||||||
| 	for { | 	for { | ||||||
| 		select { | 		select { | ||||||
| 		case <-ctx.Done(): | 		case <-ctx.Done(): | ||||||
|  | 			if ctx.Err() != context.Canceled { | ||||||
|  | 				dodosTracker.MarkAsErrored() | ||||||
|  | 			} | ||||||
| 			fmt.Printf("\r") | 			fmt.Printf("\r") | ||||||
| 			dodosTracker.MarkAsErrored() | 			time.Sleep(time.Millisecond * 500) | ||||||
| 			time.Sleep(time.Millisecond * 300) |  | ||||||
| 			pw.Stop() | 			pw.Stop() | ||||||
| 			return | 			return | ||||||
|  |  | ||||||
| @@ -48,28 +49,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" | 	"time" | ||||||
|  |  | ||||||
| 	"github.com/aykhans/dodo/config" | 	"github.com/aykhans/dodo/config" | ||||||
| 	customerrors "github.com/aykhans/dodo/custom_errors" | 	"github.com/aykhans/dodo/types" | ||||||
| 	"github.com/aykhans/dodo/utils" | 	"github.com/aykhans/dodo/utils" | ||||||
| 	"github.com/valyala/fasthttp" | 	"github.com/valyala/fasthttp" | ||||||
| ) | ) | ||||||
| @@ -26,7 +26,6 @@ type Request struct { | |||||||
| func (r *Request) Send(ctx context.Context, timeout time.Duration) (*fasthttp.Response, error) { | func (r *Request) Send(ctx context.Context, timeout time.Duration) (*fasthttp.Response, error) { | ||||||
| 	client := r.getClient() | 	client := r.getClient() | ||||||
| 	request := r.getRequest() | 	request := r.getRequest() | ||||||
| 	defer client.CloseIdleConnections() |  | ||||||
| 	defer fasthttp.ReleaseRequest(request) | 	defer fasthttp.ReleaseRequest(request) | ||||||
|  |  | ||||||
| 	response := fasthttp.AcquireResponse() | 	response := fasthttp.AcquireResponse() | ||||||
| @@ -44,9 +43,9 @@ func (r *Request) Send(ctx context.Context, timeout time.Duration) (*fasthttp.Re | |||||||
| 		return response, nil | 		return response, nil | ||||||
| 	case <-time.After(timeout): | 	case <-time.After(timeout): | ||||||
| 		fasthttp.ReleaseResponse(response) | 		fasthttp.ReleaseResponse(response) | ||||||
| 		return nil, customerrors.ErrTimeout | 		return nil, types.ErrTimeout | ||||||
| 	case <-ctx.Done(): | 	case <-ctx.Done(): | ||||||
| 		return nil, customerrors.ErrInterrupt | 		return nil, types.ErrInterrupt | ||||||
| 	} | 	} | ||||||
| } | } | ||||||
|  |  | ||||||
| @@ -75,9 +74,9 @@ func newRequest( | |||||||
|  |  | ||||||
| 	getRequest := getRequestGeneratorFunc( | 	getRequest := getRequestGeneratorFunc( | ||||||
| 		requestConfig.URL, | 		requestConfig.URL, | ||||||
|  | 		requestConfig.Params, | ||||||
| 		requestConfig.Headers, | 		requestConfig.Headers, | ||||||
| 		requestConfig.Cookies, | 		requestConfig.Cookies, | ||||||
| 		requestConfig.Params, |  | ||||||
| 		requestConfig.Method, | 		requestConfig.Method, | ||||||
| 		requestConfig.Body, | 		requestConfig.Body, | ||||||
| 		localRand, | 		localRand, | ||||||
| @@ -91,37 +90,36 @@ func newRequest( | |||||||
| 	return requests | 	return requests | ||||||
| } | } | ||||||
|  |  | ||||||
| // getRequestGeneratorFunc returns a RequestGeneratorFunc which generates HTTP requests | // getRequestGeneratorFunc returns a RequestGeneratorFunc which generates HTTP requests with the specified parameters. | ||||||
| // with the specified parameters. | // The function uses a local random number generator to select bodies, headers, cookies, and parameters if multiple options are provided. | ||||||
| // The function uses a local random number generator to select bodies, headers, cookies, and parameters |  | ||||||
| // if multiple options are provided. |  | ||||||
| func getRequestGeneratorFunc( | func getRequestGeneratorFunc( | ||||||
| 	URL *url.URL, | 	URL url.URL, | ||||||
| 	Headers map[string][]string, | 	params types.Params, | ||||||
| 	Cookies map[string][]string, | 	headers types.Headers, | ||||||
| 	Params map[string][]string, | 	cookies types.Cookies, | ||||||
| 	Method string, | 	method string, | ||||||
| 	Bodies []string, | 	bodies []string, | ||||||
| 	localRand *rand.Rand, | 	localRand *rand.Rand, | ||||||
| ) RequestGeneratorFunc { | ) RequestGeneratorFunc { | ||||||
| 	bodiesLen := len(Bodies) | 	bodiesLen := len(bodies) | ||||||
| 	getBody := func() string { return "" } | 	getBody := func() string { return "" } | ||||||
| 	if bodiesLen == 1 { | 	if bodiesLen == 1 { | ||||||
| 		getBody = func() string { return Bodies[0] } | 		getBody = func() string { return bodies[0] } | ||||||
| 	} else if bodiesLen > 1 { | 	} else if bodiesLen > 1 { | ||||||
| 		getBody = utils.RandomValueCycle(Bodies, localRand) | 		getBody = utils.RandomValueCycle(bodies, localRand) | ||||||
| 	} | 	} | ||||||
| 	getHeaders := getKeyValueSetFunc(Headers, localRand) |  | ||||||
| 	getCookies := getKeyValueSetFunc(Cookies, localRand) | 	getParams := getKeyValueGeneratorFunc(params, localRand) | ||||||
| 	getParams := getKeyValueSetFunc(Params, localRand) | 	getHeaders := getKeyValueGeneratorFunc(headers, localRand) | ||||||
|  | 	getCookies := getKeyValueGeneratorFunc(cookies, localRand) | ||||||
|  |  | ||||||
| 	return func() *fasthttp.Request { | 	return func() *fasthttp.Request { | ||||||
| 		return newFasthttpRequest( | 		return newFasthttpRequest( | ||||||
| 			URL, | 			URL, | ||||||
|  | 			getParams(), | ||||||
| 			getHeaders(), | 			getHeaders(), | ||||||
| 			getCookies(), | 			getCookies(), | ||||||
| 			getParams(), | 			method, | ||||||
| 			Method, |  | ||||||
| 			getBody(), | 			getBody(), | ||||||
| 		) | 		) | ||||||
| 	} | 	} | ||||||
| @@ -130,12 +128,12 @@ func getRequestGeneratorFunc( | |||||||
| // newFasthttpRequest creates a new fasthttp.Request object with the provided parameters. | // newFasthttpRequest creates a new fasthttp.Request object with the provided parameters. | ||||||
| // It sets the request URI, host header, headers, cookies, params, method, and body. | // It sets the request URI, host header, headers, cookies, params, method, and body. | ||||||
| func newFasthttpRequest( | func newFasthttpRequest( | ||||||
| 	URL *url.URL, | 	URL url.URL, | ||||||
| 	Headers map[string]string, | 	params []types.KeyValue[string, string], | ||||||
| 	Cookies map[string]string, | 	headers []types.KeyValue[string, string], | ||||||
| 	Params map[string]string, | 	cookies []types.KeyValue[string, string], | ||||||
| 	Method string, | 	method string, | ||||||
| 	Body string, | 	body string, | ||||||
| ) *fasthttp.Request { | ) *fasthttp.Request { | ||||||
| 	request := fasthttp.AcquireRequest() | 	request := fasthttp.AcquireRequest() | ||||||
| 	request.SetRequestURI(URL.Path) | 	request.SetRequestURI(URL.Path) | ||||||
| @@ -143,12 +141,12 @@ func newFasthttpRequest( | |||||||
| 	// Set the host of the request to the host header | 	// Set the host of the request to the host header | ||||||
| 	// If the host header is not set, the request will fail | 	// If the host header is not set, the request will fail | ||||||
| 	// If there is host header in the headers, it will be overwritten | 	// If there is host header in the headers, it will be overwritten | ||||||
| 	request.Header.Set("Host", URL.Host) | 	request.Header.SetHost(URL.Host) | ||||||
| 	setRequestHeaders(request, Headers) | 	setRequestParams(request, params) | ||||||
| 	setRequestCookies(request, Cookies) | 	setRequestHeaders(request, headers) | ||||||
| 	setRequestParams(request, Params) | 	setRequestCookies(request, cookies) | ||||||
| 	setRequestMethod(request, Method) | 	setRequestMethod(request, method) | ||||||
| 	setRequestBody(request, Body) | 	setRequestBody(request, body) | ||||||
| 	if URL.Scheme == "https" { | 	if URL.Scheme == "https" { | ||||||
| 		request.URI().SetScheme("https") | 		request.URI().SetScheme("https") | ||||||
| 	} | 	} | ||||||
| @@ -156,28 +154,28 @@ func newFasthttpRequest( | |||||||
| 	return request | 	return request | ||||||
| } | } | ||||||
|  |  | ||||||
| // setRequestHeaders sets the headers of the given request with the provided key-value pairs. | // setRequestParams adds the query parameters of the given request based on the provided key-value pairs. | ||||||
| func setRequestHeaders(req *fasthttp.Request, headers map[string]string) { | func setRequestParams(req *fasthttp.Request, params []types.KeyValue[string, string]) { | ||||||
| 	req.Header.Set("User-Agent", config.DefaultUserAgent) | 	for _, param := range params { | ||||||
| 	for key, value := range headers { | 		req.URI().QueryArgs().Add(param.Key, param.Value) | ||||||
| 		req.Header.Set(key, value) |  | ||||||
| 	} | 	} | ||||||
| } | } | ||||||
|  |  | ||||||
| // setRequestCookies sets the cookies in the given request. | // setRequestHeaders adds the headers of the given request with the provided key-value pairs. | ||||||
| func setRequestCookies(req *fasthttp.Request, cookies map[string]string) { | func setRequestHeaders(req *fasthttp.Request, headers []types.KeyValue[string, string]) { | ||||||
| 	for key, value := range cookies { | 	for _, header := range headers { | ||||||
| 		req.Header.SetCookie(key, value) | 		req.Header.Add(header.Key, header.Value) | ||||||
|  | 	} | ||||||
|  | 	if req.Header.UserAgent() == nil { | ||||||
|  | 		req.Header.SetUserAgent(config.DefaultUserAgent) | ||||||
| 	} | 	} | ||||||
| } | } | ||||||
|  |  | ||||||
| // setRequestParams sets the query parameters of the given request based on the provided map of key-value pairs. | // setRequestCookies adds the cookies of the given request with the provided key-value pairs. | ||||||
| func setRequestParams(req *fasthttp.Request, params map[string]string) { | func setRequestCookies(req *fasthttp.Request, cookies []types.KeyValue[string, string]) { | ||||||
| 	urlParams := url.Values{} | 	for _, cookie := range cookies { | ||||||
| 	for key, value := range params { | 		req.Header.Add("Cookie", cookie.Key+"="+cookie.Value) | ||||||
| 		urlParams.Add(key, value) |  | ||||||
| 	} | 	} | ||||||
| 	req.URI().SetQueryString(urlParams.Encode()) |  | ||||||
| } | } | ||||||
|  |  | ||||||
| // setRequestMethod sets the HTTP request method for the given request. | // setRequestMethod sets the HTTP request method for the given request. | ||||||
| @@ -191,59 +189,62 @@ func setRequestBody(req *fasthttp.Request, body string) { | |||||||
| 	req.SetBody([]byte(body)) | 	req.SetBody([]byte(body)) | ||||||
| } | } | ||||||
|  |  | ||||||
| // getKeyValueSetFunc generates a function that returns a map of key-value pairs based on the provided key-value set. | // getKeyValueGeneratorFunc creates a function that generates key-value pairs for HTTP requests. | ||||||
| // The generated function will either return fixed values or random values depending on the input. | // It takes a slice of key-value pairs where each key maps to a slice of possible values, | ||||||
|  | // and a random number generator. | ||||||
| // | // | ||||||
| // Returns: | // If any key has multiple possible values, the function will randomly select one value for each | ||||||
| //   - A function that returns a map of key-value pairs. If the input map contains multiple values for a key, | // call (using the provided random number generator). If all keys have at most one value, the | ||||||
| //     the returned function will generate random values for that key. If the input map contains a single value | // function will always return the same set of key-value pairs for efficiency. | ||||||
| //     for a key, the returned function will always return that value. If the input map is empty for a key, | func getKeyValueGeneratorFunc[ | ||||||
| //     the returned function will generate an empty string for that key. | 	T []types.KeyValue[string, string], | ||||||
| func getKeyValueSetFunc[ | ]( | ||||||
| 	KeyValueSet map[string][]string, | 	keyValueSlice []types.KeyValue[string, []string], | ||||||
| 	KeyValue map[string]string, | 	localRand *rand.Rand, | ||||||
| ](keyValueSet KeyValueSet, localRand *rand.Rand) func() KeyValue { | ) func() T { | ||||||
| 	getKeyValueSlice := []map[string]func() string{} | 	getKeyValueSlice := []map[string]func() string{} | ||||||
| 	isRandom := false | 	isRandom := false | ||||||
| 	for key, values := range keyValueSet { |  | ||||||
| 		valuesLen := len(values) |  | ||||||
|  |  | ||||||
| 		// if values is empty, return a function that generates empty string | 	for _, kv := range keyValueSlice { | ||||||
| 		// if values has only one element, return a function that generates that element | 		valuesLen := len(kv.Value) | ||||||
| 		// if values has more than one element, return a function that generates a random element |  | ||||||
| 		getKeyValue := func() string { return "" } | 		getValueFunc := func() string { return "" } | ||||||
| 		if valuesLen == 1 { | 		if valuesLen == 1 { | ||||||
| 			getKeyValue = func() string { return values[0] } | 			getValueFunc = func() string { return kv.Value[0] } | ||||||
| 		} else if valuesLen > 1 { | 		} else if valuesLen > 1 { | ||||||
| 			getKeyValue = utils.RandomValueCycle(values, localRand) | 			getValueFunc = utils.RandomValueCycle(kv.Value, localRand) | ||||||
| 			isRandom = true | 			isRandom = true | ||||||
| 		} | 		} | ||||||
|  |  | ||||||
| 		getKeyValueSlice = append( | 		getKeyValueSlice = append( | ||||||
| 			getKeyValueSlice, | 			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 { | 	if isRandom { | ||||||
| 		return func() KeyValue { | 		return func() T { | ||||||
| 			keyValues := make(KeyValue, len(getKeyValueSlice)) | 			keyValues := make(T, len(getKeyValueSlice)) | ||||||
| 			for _, keyValue := range getKeyValueSlice { | 			for i, keyValue := range getKeyValueSlice { | ||||||
| 				for key, value := range keyValue { | 				for key, value := range keyValue { | ||||||
| 					keyValues[key] = value() | 					keyValues[i] = types.KeyValue[string, string]{ | ||||||
|  | 						Key:   key, | ||||||
|  | 						Value: value(), | ||||||
|  | 					} | ||||||
| 				} | 				} | ||||||
| 			} | 			} | ||||||
| 			return keyValues | 			return keyValues | ||||||
| 		} | 		} | ||||||
| 	} else { | 	} else { | ||||||
| 		keyValues := make(KeyValue, len(getKeyValueSlice)) | 		keyValues := make(T, len(getKeyValueSlice)) | ||||||
| 		for _, keyValue := range getKeyValueSlice { | 		for i, keyValue := range getKeyValueSlice { | ||||||
| 			for key, value := range keyValue { | 			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,8 @@ import ( | |||||||
| 	"os" | 	"os" | ||||||
| 	"time" | 	"time" | ||||||
|  |  | ||||||
| 	. "github.com/aykhans/dodo/types" | 	"github.com/aykhans/dodo/types" | ||||||
|  | 	"github.com/aykhans/dodo/utils" | ||||||
| 	"github.com/jedib0t/go-pretty/v6/table" | 	"github.com/jedib0t/go-pretty/v6/table" | ||||||
| ) | ) | ||||||
|  |  | ||||||
| @@ -17,7 +18,7 @@ type Responses []*Response | |||||||
|  |  | ||||||
| // Print prints the responses in a tabular format, including information such as | // Print prints the responses in a tabular format, including information such as | ||||||
| // response count, minimum time, maximum time, average time, and latency percentiles. | // response count, minimum time, maximum time, average time, and latency percentiles. | ||||||
| func (respones Responses) Print() { | func (responses Responses) Print() { | ||||||
| 	total := struct { | 	total := struct { | ||||||
| 		Count int | 		Count int | ||||||
| 		Min   time.Duration | 		Min   time.Duration | ||||||
| @@ -27,14 +28,14 @@ func (respones Responses) Print() { | |||||||
| 		P95   time.Duration | 		P95   time.Duration | ||||||
| 		P99   time.Duration | 		P99   time.Duration | ||||||
| 	}{ | 	}{ | ||||||
| 		Count: len(respones), | 		Count: len(responses), | ||||||
| 		Min:   respones[0].Time, | 		Min:   responses[0].Time, | ||||||
| 		Max:   respones[0].Time, | 		Max:   responses[0].Time, | ||||||
| 	} | 	} | ||||||
| 	mergedResponses := make(map[string]Durations) | 	mergedResponses := make(map[string]types.Durations) | ||||||
| 	var allDurations Durations | 	var allDurations types.Durations | ||||||
|  |  | ||||||
| 	for _, response := range respones { | 	for _, response := range responses { | ||||||
| 		if response.Time < total.Min { | 		if response.Time < total.Min { | ||||||
| 			total.Min = response.Time | 			total.Min = response.Time | ||||||
| 		} | 		} | ||||||
| @@ -64,14 +65,15 @@ func (respones Responses) Print() { | |||||||
| 	t.AppendHeader(table.Row{ | 	t.AppendHeader(table.Row{ | ||||||
| 		"Response", | 		"Response", | ||||||
| 		"Count", | 		"Count", | ||||||
| 		"Min Time", | 		"Min", | ||||||
| 		"Max Time", | 		"Max", | ||||||
| 		"Average Time", | 		"Average", | ||||||
| 		"P90", | 		"P90", | ||||||
| 		"P95", | 		"P95", | ||||||
| 		"P99", | 		"P99", | ||||||
| 	}) | 	}) | ||||||
|  |  | ||||||
|  | 	var roundPrecision int64 = 4 | ||||||
| 	for key, durations := range mergedResponses { | 	for key, durations := range mergedResponses { | ||||||
| 		durations.Sort() | 		durations.Sort() | ||||||
| 		durationsLen := len(durations) | 		durationsLen := len(durations) | ||||||
| @@ -80,12 +82,12 @@ func (respones Responses) Print() { | |||||||
| 		t.AppendRow(table.Row{ | 		t.AppendRow(table.Row{ | ||||||
| 			key, | 			key, | ||||||
| 			durationsLen, | 			durationsLen, | ||||||
| 			durations.First(), | 			utils.DurationRoundBy(*durations.First(), roundPrecision), | ||||||
| 			durations.Last(), | 			utils.DurationRoundBy(*durations.Last(), roundPrecision), | ||||||
| 			durations.Avg(), | 			utils.DurationRoundBy(durations.Avg(), roundPrecision), | ||||||
| 			durations[int(0.90*durationsLenAsFloat)], | 			utils.DurationRoundBy(durations[int(0.90*durationsLenAsFloat)], roundPrecision), | ||||||
| 			durations[int(0.95*durationsLenAsFloat)], | 			utils.DurationRoundBy(durations[int(0.95*durationsLenAsFloat)], roundPrecision), | ||||||
| 			durations[int(0.99*durationsLenAsFloat)], | 			utils.DurationRoundBy(durations[int(0.99*durationsLenAsFloat)], roundPrecision), | ||||||
| 		}) | 		}) | ||||||
| 		t.AppendSeparator() | 		t.AppendSeparator() | ||||||
| 	} | 	} | ||||||
| @@ -94,12 +96,12 @@ func (respones Responses) Print() { | |||||||
| 		t.AppendRow(table.Row{ | 		t.AppendRow(table.Row{ | ||||||
| 			"Total", | 			"Total", | ||||||
| 			total.Count, | 			total.Count, | ||||||
| 			total.Min, | 			utils.DurationRoundBy(total.Min, roundPrecision), | ||||||
| 			total.Max, | 			utils.DurationRoundBy(total.Max, roundPrecision), | ||||||
| 			total.Sum / time.Duration(total.Count), // Average | 			utils.DurationRoundBy(total.Sum/time.Duration(total.Count), roundPrecision), // Average | ||||||
| 			total.P90, | 			utils.DurationRoundBy(total.P90, roundPrecision), | ||||||
| 			total.P95, | 			utils.DurationRoundBy(total.P95, roundPrecision), | ||||||
| 			total.P99, | 			utils.DurationRoundBy(total.P99, roundPrecision), | ||||||
| 		}) | 		}) | ||||||
| 	} | 	} | ||||||
| 	t.Render() | 	t.Render() | ||||||
|   | |||||||
| @@ -7,48 +7,33 @@ import ( | |||||||
| 	"time" | 	"time" | ||||||
|  |  | ||||||
| 	"github.com/aykhans/dodo/config" | 	"github.com/aykhans/dodo/config" | ||||||
| 	customerrors "github.com/aykhans/dodo/custom_errors" | 	"github.com/aykhans/dodo/types" | ||||||
| 	"github.com/aykhans/dodo/utils" | 	"github.com/aykhans/dodo/utils" | ||||||
| 	"github.com/valyala/fasthttp" | 	"github.com/valyala/fasthttp" | ||||||
| ) | ) | ||||||
|  |  | ||||||
| // Run executes the main logic for processing requests based on the provided configuration. | // 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 initializes clients based on the request configuration and releases the dodos. | ||||||
| // it returns an error. Then, it initializes clients based on the request configuration and | // If the context is canceled and no responses are collected, it returns an interrupt error. | ||||||
| // releases the dodos. If the context is canceled and no responses are collected, it returns an interrupt error. |  | ||||||
| // | // | ||||||
| // Parameters: | // Parameters: | ||||||
| //   - ctx: The context for managing request lifecycle and cancellation. | //   - ctx: The context for managing request lifecycle and cancellation. | ||||||
| //   - requestConfig: The configuration for the request, including timeout, proxies, and other settings. | //   - requestConfig: The configuration for the request, including timeout, proxies, and other settings. | ||||||
| // |  | ||||||
| // 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) { | 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( | 	clients := getClients( | ||||||
| 		ctx, | 		ctx, | ||||||
| 		requestConfig.Timeout, | 		requestConfig.Timeout, | ||||||
| 		requestConfig.Proxies, | 		requestConfig.Proxies, | ||||||
| 		requestConfig.GetValidDodosCountForProxies(), |  | ||||||
| 		requestConfig.GetMaxConns(fasthttp.DefaultMaxConnsPerHost), | 		requestConfig.GetMaxConns(fasthttp.DefaultMaxConnsPerHost), | ||||||
| 		requestConfig.Yes, |  | ||||||
| 		requestConfig.NoProxyCheck, |  | ||||||
| 		requestConfig.URL, | 		requestConfig.URL, | ||||||
| 	) | 	) | ||||||
| 	if clients == nil { | 	if clients == nil { | ||||||
| 		return nil, customerrors.ErrInterrupt | 		return nil, types.ErrInterrupt | ||||||
| 	} | 	} | ||||||
|  |  | ||||||
| 	responses := releaseDodos(ctx, requestConfig, clients) | 	responses := releaseDodos(ctx, requestConfig, clients) | ||||||
| 	if ctx.Err() != nil && len(responses) == 0 { | 	if ctx.Err() != nil && len(responses) == 0 { | ||||||
| 		return nil, customerrors.ErrInterrupt | 		return nil, types.ErrInterrupt | ||||||
| 	} | 	} | ||||||
|  |  | ||||||
| 	return responses, nil | 	return responses, nil | ||||||
| @@ -75,23 +60,22 @@ func releaseDodos( | |||||||
| 		requestCountPerDodo uint | 		requestCountPerDodo uint | ||||||
| 		dodosCount          uint = requestConfig.GetValidDodosCountForRequests() | 		dodosCount          uint = requestConfig.GetValidDodosCountForRequests() | ||||||
| 		dodosCountInt       int  = int(dodosCount) | 		dodosCountInt       int  = int(dodosCount) | ||||||
| 		requestCount        uint = uint(requestConfig.RequestCount) |  | ||||||
| 		responses                = make([][]*Response, dodosCount) | 		responses                = make([][]*Response, dodosCount) | ||||||
| 		increase                 = make(chan int64, requestCount) | 		increase                 = make(chan int64, requestConfig.RequestCount) | ||||||
| 	) | 	) | ||||||
|  |  | ||||||
| 	wg.Add(dodosCountInt) | 	wg.Add(dodosCountInt) | ||||||
| 	streamWG.Add(1) | 	streamWG.Add(1) | ||||||
| 	streamCtx, streamCtxCancel := context.WithCancel(context.Background()) | 	streamCtx, streamCtxCancel := context.WithCancel(context.Background()) | ||||||
|  |  | ||||||
| 	go streamProgress(streamCtx, &streamWG, int64(requestCount), "Dodos Working🔥", increase) | 	go streamProgress(streamCtx, &streamWG, int64(requestConfig.RequestCount), "Dodos Working🔥", increase) | ||||||
|  |  | ||||||
| 	for i := range dodosCount { | 	for i := range dodosCount { | ||||||
| 		if i+1 == dodosCount { | 		if i+1 == dodosCount { | ||||||
| 			requestCountPerDodo = requestCount - (i * requestCount / dodosCount) | 			requestCountPerDodo = requestConfig.RequestCount - (i * requestConfig.RequestCount / dodosCount) | ||||||
| 		} else { | 		} else { | ||||||
| 			requestCountPerDodo = ((i + 1) * requestCount / dodosCount) - | 			requestCountPerDodo = ((i + 1) * requestConfig.RequestCount / dodosCount) - | ||||||
| 				(i * requestCount / dodosCount) | 				(i * requestConfig.RequestCount / dodosCount) | ||||||
| 		} | 		} | ||||||
|  |  | ||||||
| 		go sendRequest( | 		go sendRequest( | ||||||
| @@ -139,7 +123,7 @@ func sendRequest( | |||||||
| 			} | 			} | ||||||
|  |  | ||||||
| 			if err != nil { | 			if err != nil { | ||||||
| 				if err == customerrors.ErrInterrupt { | 				if err == types.ErrInterrupt { | ||||||
| 					return | 					return | ||||||
| 				} | 				} | ||||||
| 				*responseData = append(*responseData, &Response{ | 				*responseData = append(*responseData, &Response{ | ||||||
|   | |||||||
							
								
								
									
										72
									
								
								types/body.go
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										72
									
								
								types/body.go
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,72 @@ | |||||||
|  | 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) Set(value string) error { | ||||||
|  | 	*body = append(*body, value) | ||||||
|  | 	return nil | ||||||
|  | } | ||||||
							
								
								
									
										23
									
								
								types/config_file.go
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										23
									
								
								types/config_file.go
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,23 @@ | |||||||
|  | package types | ||||||
|  |  | ||||||
|  | import "strings" | ||||||
|  |  | ||||||
|  | type FileLocationType int | ||||||
|  |  | ||||||
|  | const ( | ||||||
|  | 	FileLocationTypeLocal FileLocationType = iota | ||||||
|  | 	FileLocationTypeRemoteHTTP | ||||||
|  | ) | ||||||
|  |  | ||||||
|  | type ConfigFile string | ||||||
|  |  | ||||||
|  | func (config ConfigFile) String() string { | ||||||
|  | 	return string(config) | ||||||
|  | } | ||||||
|  |  | ||||||
|  | func (config ConfigFile) LocationType() FileLocationType { | ||||||
|  | 	if strings.HasPrefix(string(config), "http://") || strings.HasPrefix(string(config), "https://") { | ||||||
|  | 		return FileLocationTypeRemoteHTTP | ||||||
|  | 	} | ||||||
|  | 	return FileLocationTypeLocal | ||||||
|  | } | ||||||
							
								
								
									
										114
									
								
								types/cookies.go
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										114
									
								
								types/cookies.go
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,114 @@ | |||||||
|  | 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) 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) 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 | ||||||
|  | } | ||||||
|  |  | ||||||
|  | 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 | ||||||
|  | } | ||||||
							
								
								
									
										36
									
								
								types/duration.go
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										36
									
								
								types/duration.go
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,36 @@ | |||||||
|  | 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()) | ||||||
|  | } | ||||||
							
								
								
									
										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") | ||||||
|  | ) | ||||||
							
								
								
									
										114
									
								
								types/headers.go
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										114
									
								
								types/headers.go
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,114 @@ | |||||||
|  | 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) 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) 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) 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 | ||||||
|  | } | ||||||
							
								
								
									
										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} |  | ||||||
| } |  | ||||||
							
								
								
									
										114
									
								
								types/params.go
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										114
									
								
								types/params.go
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,114 @@ | |||||||
|  | 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) 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) 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 | ||||||
|  | } | ||||||
|  |  | ||||||
|  | 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 | ||||||
|  | } | ||||||
							
								
								
									
										86
									
								
								types/proxies.go
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										86
									
								
								types/proxies.go
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,86 @@ | |||||||
|  | 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) Set(value string) error { | ||||||
|  | 	parsedURL, err := url.Parse(value) | ||||||
|  | 	if err != nil { | ||||||
|  | 		return err | ||||||
|  | 	} | ||||||
|  |  | ||||||
|  | 	*proxies = append(*proxies, *parsedURL) | ||||||
|  | 	return nil | ||||||
|  | } | ||||||
							
								
								
									
										44
									
								
								types/request_url.go
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										44
									
								
								types/request_url.go
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,44 @@ | |||||||
|  | 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) 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 | ||||||
|  | } | ||||||
							
								
								
									
										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,54 +1,5 @@ | |||||||
| package utils | package utils | ||||||
|  |  | ||||||
| import ( | func ToPtr[T any](value T) *T { | ||||||
| 	"encoding/json" | 	return &value | ||||||
| 	"fmt" |  | ||||||
| 	"reflect" |  | ||||||
| ) |  | ||||||
|  |  | ||||||
| func MarshalJSON(v any, maxSliceSize uint) string { |  | ||||||
| 	rv := reflect.ValueOf(v) |  | ||||||
| 	if rv.Kind() == reflect.Slice && rv.Len() == 0 { |  | ||||||
| 		return "[]" |  | ||||||
| 	} |  | ||||||
|  |  | ||||||
| 	data, err := json.MarshalIndent(truncateLists(v, int(maxSliceSize)), "", "  ") |  | ||||||
| 	if err != nil { |  | ||||||
| 		return "{}" |  | ||||||
| 	} |  | ||||||
|  |  | ||||||
| 	return string(data) |  | ||||||
| } |  | ||||||
|  |  | ||||||
| func truncateLists(v interface{}, maxItems int) interface{} { |  | ||||||
| 	rv := reflect.ValueOf(v) |  | ||||||
|  |  | ||||||
| 	switch rv.Kind() { |  | ||||||
| 	case reflect.Slice, reflect.Array: |  | ||||||
| 		if rv.Len() > maxItems { |  | ||||||
| 			newSlice := reflect.MakeSlice(rv.Type(), maxItems, maxItems) |  | ||||||
| 			reflect.Copy(newSlice, rv.Slice(0, maxItems)) |  | ||||||
| 			newSlice = reflect.Append(newSlice, reflect.ValueOf(fmt.Sprintf("...(%d more)", rv.Len()-maxItems))) |  | ||||||
| 			return newSlice.Interface() |  | ||||||
| 		} |  | ||||||
| 	case reflect.Map: |  | ||||||
| 		newMap := reflect.MakeMap(rv.Type()) |  | ||||||
| 		for _, key := range rv.MapKeys() { |  | ||||||
| 			newMap.SetMapIndex(key, reflect.ValueOf(truncateLists(rv.MapIndex(key).Interface(), maxItems))) |  | ||||||
| 		} |  | ||||||
| 		return newMap.Interface() |  | ||||||
| 	case reflect.Struct: |  | ||||||
| 		newStruct := reflect.New(rv.Type()).Elem() |  | ||||||
| 		for i := 0; i < rv.NumField(); i++ { |  | ||||||
| 			newStruct.Field(i).Set(reflect.ValueOf(truncateLists(rv.Field(i).Interface(), maxItems))) |  | ||||||
| 		} |  | ||||||
| 		return newStruct.Interface() |  | ||||||
| 	case reflect.Ptr: |  | ||||||
| 		if rv.IsNil() { |  | ||||||
| 			return nil |  | ||||||
| 		} |  | ||||||
| 		return truncateLists(rv.Elem().Interface(), maxItems) |  | ||||||
| 	} |  | ||||||
|  |  | ||||||
| 	return v |  | ||||||
| } | } | ||||||
|   | |||||||
							
								
								
									
										21
									
								
								utils/int.go
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										21
									
								
								utils/int.go
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,21 @@ | |||||||
|  | package utils | ||||||
|  |  | ||||||
|  | type Number interface { | ||||||
|  | 	int | int8 | int16 | int32 | int64 | ||||||
|  | } | ||||||
|  |  | ||||||
|  | func NumLen[T Number](n T) T { | ||||||
|  | 	if n < 0 { | ||||||
|  | 		n = -n | ||||||
|  | 	} | ||||||
|  | 	if n == 0 { | ||||||
|  | 		return 1 | ||||||
|  | 	} | ||||||
|  |  | ||||||
|  | 	var count T = 0 | ||||||
|  | 	for n > 0 { | ||||||
|  | 		n /= 10 | ||||||
|  | 		count++ | ||||||
|  | 	} | ||||||
|  | 	return count | ||||||
|  | } | ||||||
| @@ -3,46 +3,12 @@ package utils | |||||||
| import ( | import ( | ||||||
| 	"fmt" | 	"fmt" | ||||||
| 	"os" | 	"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) { | func PrintErr(err error) { | ||||||
| 	PrintlnC(Colors.Red, err.Error()) | 	fmt.Fprintln(os.Stderr, text.FgRed.Sprint(err.Error())) | ||||||
| } | } | ||||||
|  |  | ||||||
| func PrintErrAndExit(err error) { | func PrintErrAndExit(err error) { | ||||||
|   | |||||||
| @@ -10,15 +10,6 @@ func Flatten[T any](nested [][]*T) []*T { | |||||||
| 	return flattened | 	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 | // 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. | // in a random order. Each call to the returned function will yield a value from the slice. | ||||||
| // The order of values is determined by the provided random number generator. | // The order of values is determined by the provided random number generator. | ||||||
|   | |||||||
							
								
								
									
										14
									
								
								utils/time.go
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										14
									
								
								utils/time.go
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,14 @@ | |||||||
|  | package utils | ||||||
|  |  | ||||||
|  | import "time" | ||||||
|  |  | ||||||
|  | func DurationRoundBy(duration time.Duration, n int64) time.Duration { | ||||||
|  | 	if durationLen := NumLen(duration.Nanoseconds()); durationLen > n { | ||||||
|  | 		roundNum := 1 | ||||||
|  | 		for range durationLen - n { | ||||||
|  | 			roundNum *= 10 | ||||||
|  | 		} | ||||||
|  | 		return duration.Round(time.Duration(roundNum)) | ||||||
|  | 	} | ||||||
|  | 	return duration | ||||||
|  | } | ||||||
| @@ -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