From 00f0bcb2dedc0ee15e90eb29bee0749a80667a04 Mon Sep 17 00:00:00 2001 From: Aykhan Shahsuvarov Date: Sun, 16 Mar 2025 21:20:33 +0400 Subject: [PATCH] =?UTF-8?q?=F0=9F=94=A8=20Restructure=20entire=20project?= =?UTF-8?q?=20logic?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - Moved readers to the config package - Added an option to read remote config files - Moved the validation package to the config package and removed the validator dependency - Moved the customerrors package to the config package - Replaced fatih/color with jedib0t/go-pretty/v6/text - Removed proxy check functionality - Added param, header, cookie, body, and proxy flags to the CLI - Allowed multiple values for the same key in params, headers, and cookies --- Dockerfile | 10 +- README.md | 319 +++++++++++++++++++++---------------- config.json | 58 +++---- config.yaml | 39 +++++ config/cli.go | 175 ++++++++++++++++++++ config/config.go | 309 +++++++++++++++++------------------ config/file.go | 60 +++++++ custom_errors/errors.go | 117 -------------- custom_errors/formaters.go | 68 -------- go.mod | 19 +-- go.sum | 39 +---- main.go | 128 +++++---------- readers/cli.go | 155 ------------------ readers/json.go | 66 -------- requests/client.go | 288 ++++----------------------------- requests/helper.go | 32 +--- requests/request.go | 160 ++++++++++--------- requests/response.go | 6 +- requests/run.go | 27 +--- types/body.go | 72 +++++++++ types/config_file.go | 23 +++ types/cookies.go | 114 +++++++++++++ types/duration.go | 36 +++++ types/errors.go | 10 ++ types/headers.go | 114 +++++++++++++ types/key_value.go | 6 + types/option.go | 89 ----------- types/params.go | 114 +++++++++++++ types/proxies.go | 86 ++++++++++ types/request_url.go | 44 +++++ utils/compare.go | 14 ++ utils/convert.go | 84 +--------- utils/print.go | 4 +- utils/slice.go | 9 -- validation/validator.go | 59 ------- 35 files changed, 1461 insertions(+), 1492 deletions(-) create mode 100644 config.yaml create mode 100644 config/cli.go create mode 100644 config/file.go delete mode 100644 custom_errors/errors.go delete mode 100644 custom_errors/formaters.go delete mode 100644 readers/cli.go delete mode 100644 readers/json.go create mode 100644 types/body.go create mode 100644 types/config_file.go create mode 100644 types/cookies.go create mode 100644 types/duration.go create mode 100644 types/errors.go create mode 100644 types/headers.go create mode 100644 types/key_value.go delete mode 100644 types/option.go create mode 100644 types/params.go create mode 100644 types/proxies.go create mode 100644 types/request_url.go create mode 100644 utils/compare.go delete mode 100644 validation/validator.go diff --git a/Dockerfile b/Dockerfile index 9de347c..29ea12c 100644 --- a/Dockerfile +++ b/Dockerfile @@ -1,6 +1,6 @@ FROM golang:1.24-alpine AS builder -WORKDIR /dodo +WORKDIR /src COPY go.mod go.sum ./ RUN go mod download @@ -11,9 +11,9 @@ RUN echo "{}" > config.json FROM gcr.io/distroless/static-debian12:latest -WORKDIR /dodo +WORKDIR / -COPY --from=builder /dodo/dodo /dodo/dodo -COPY --from=builder /dodo/config.json /dodo/config.json +COPY --from=builder /src/dodo /dodo +COPY --from=builder /src/config.json /config.json -ENTRYPOINT ["./dodo", "-c", "/dodo/config.json"] \ No newline at end of file +ENTRYPOINT ["./dodo", "-f", "/config.json"] \ No newline at end of file diff --git a/README.md b/README.md index 5d31bf7..370cabe 100644 --- a/README.md +++ b/README.md @@ -1,133 +1,186 @@ -

Dodo is a simple and easy-to-use HTTP benchmarking tool.

-

- -

- -## Installation -### With Docker (Recommended) -Pull the Dodo image from Docker Hub: -```sh -docker pull aykhans/dodo:latest -``` -If you use Dodo with Docker and a config file, you must provide the config.json file as a volume to the Docker run command (not as the "-c config.json" argument), as shown in the examples in the [usage](#usage) section. - -### With Binary File -You can grab binaries in the [releases](https://github.com/aykhans/dodo/releases) section. - -### Build from Source -To build Dodo from source, you need to have [Go1.22+](https://golang.org/dl/) installed.
-Follow the steps below to build dodo: - -1. **Clone the repository:** - - ```sh - git clone https://github.com/aykhans/dodo.git - ``` - -2. **Navigate to the project directory:** - - ```sh - cd dodo - ``` - -3. **Build the project:** - - ```sh - go build -ldflags "-s -w" -o dodo - ``` - -This will generate an executable named `dodo` in the project directory. - -## Usage -You can use Dodo with CLI arguments, a JSON config file, or both. If you use both, CLI arguments will always override JSON config arguments if there is a conflict. - -### 1. CLI -Send 1000 GET requests to https://example.com with 10 parallel dodos (threads) and a timeout of 2000 milliseconds: - -```sh -dodo -u https://example.com -m GET -d 10 -r 1000 -t 2000 -``` -With Docker: -```sh -docker run --rm -i aykhans/dodo -u https://example.com -m GET -d 10 -r 1000 -t 2000 -``` - -### 2. JSON config file -You can find an example config structure in the [config.json](https://github.com/aykhans/dodo/blob/main/config.json) file: -```jsonc -{ - "method": "GET", - "url": "https://example.com", - "no_proxy_check": false, - "timeout": 10000, - "dodos": 1, - "requests": 1, - "params": { - // Random param value will be selected from the param-key1 and param-key2 list for each request - "param-key1": ["param-value1", "param-value2", "param-value3"], - "param-key2": ["param-value1", "param-value2", "param-value3"] - }, - "headers": { - // Random header value will be selected from the header-key1 and header-key2 list for each request - "header-key1": ["header-value1", "header-value2", "header-value3"], - "header-key2": ["header-value2", "header-value2", "header-value3"] - }, - "cookies": { - // Random cookie value will be selected from the cookie-key1 and cookie-key2 list for each request - "cookie-key1": ["cookie-value1", "cookie-value2", "cookie-value3"], - "cookie-key2": ["cookie-value2", "cookie-value2", "cookie-value3"] - }, - // Random body value will be selected from the body list for each request - "body": ["body1", "body2", "body3"], - // Random proxy will be selected from the proxy list for each request - "proxies": [ - { - "url": "http://example.com:8080", - "username": "username", - "password": "password" - }, - { - "url": "http://example.com:8080" - } - ] -} -``` -Send 1000 GET requests to https://example.com with 10 parallel dodos (threads) and a timeout of 2000 milliseconds: - -```sh -dodo -c /path/config.json -``` -With Docker: -```sh -docker run --rm -i -v ./path/config.json:/dodo/config.json aykhans/dodo -``` - -### 3. Both (CLI & JSON) -Override the config file arguments with CLI arguments: - -```sh -dodo -c /path/config.json -u https://example.com -m GET -d 10 -r 1000 -t 2000 -``` -With Docker: -```sh -docker run --rm -i -v ./path/config.json:/dodo/config.json aykhans/dodo -u https://example.com -m GET -d 10 -r 1000 -t 2000 -``` - -## CLI and JSON Config Parameters -If the Headers, Params, Cookies and Body fields have multiple values, each request will choose a random value from the list. - -| Parameter | JSON config file | CLI Flag | CLI Short Flag | Type | Description | Default | -| --------------------- | ---------------- | --------------- | -------------- | -------------------------------- | ------------------------------------------------------------------- | ----------- | -| Config file | - | --config-file | -c | String | Path to the JSON config file | - | -| Yes | - | --yes | -y | Boolean | Answer yes to all questions | false | -| URL | url | --url | -u | String | URL to send the request to | - | -| Method | method | --method | -m | String | HTTP method | GET | -| Requests | requests | --requests | -r | Integer | Total number of requests to send | 1000 | -| Dodos (Threads) | dodos | --dodos | -d | Integer | Number of dodos (threads) to send requests in parallel | 1 | -| Timeout | timeout | --timeout | -t | Integer | Timeout for canceling each request (milliseconds) | 10000 | -| No Proxy Check | no_proxy_check | --no-proxy-check| - | Boolean | Disable proxy check | false | -| Params | params | - | - | Key-Value {String: [String]} | Request parameters | - | -| Headers | headers | - | - | Key-Value {String: [String]} | Request headers | - | -| Cookies | cookies | - | - | Key-Value {String: [String]} | Request cookies | - | -| Body | body | - | - | [String] | Request body | - | -| Proxy | proxies | - | - | List[Key-Value {string: string}] | List of proxies (will check active proxies before sending requests) | - | +

Dodo is a fast and easy-to-use HTTP benchmarking tool.

+

+ +

+ +## Installation + +### With Docker (Recommended) + +Pull the Dodo image from Docker Hub: + +```sh +docker pull aykhans/dodo:latest +``` + +If you use Dodo with Docker and a 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). + +```sh +docker run -v /path/to/config.json:/config.json aykhans/dodo +``` + +If you use it with Docker and provide config file via URL, you do not need to set a volume. + +```sh +docker run aykhans/dodo -f https://raw.githubusercontent.com/aykhans/dodo/main/config.json +``` + +### With Binary File + +You can grab binaries in the [releases](https://github.com/aykhans/dodo/releases) section. + +### Build from Source + +To build Dodo from source, you need to have [Go1.24+](https://golang.org/dl/) installed.
+Follow the steps below to build dodo: + +1. **Clone the repository:** + + ```sh + git clone https://github.com/aykhans/dodo.git + ``` + +2. **Navigate to the project directory:** + + ```sh + cd dodo + ``` + +3. **Build the project:** + + ```sh + go build -ldflags "-s -w" -o dodo + ``` + +This will generate an executable named `dodo` in the project directory. + +## Usage + +You can use Dodo with CLI arguments, a JSON config file, or both. If you use both, CLI arguments will always override JSON config arguments if there is a conflict. + +### 1. CLI + +Send 1000 GET requests to https://example.com with 10 parallel dodos (threads) and a timeout of 2 seconds: + +```sh +dodo -u https://example.com -m GET -d 10 -r 1000 -t 2s +``` + +With Docker: + +```sh +docker run --rm -i aykhans/dodo -u https://example.com -m GET -d 10 -r 1000 -t 2s +``` + +### 2. JSON config file + +Send 1000 GET requests to https://example.com with 10 parallel dodos (threads) and a timeout of 800 milliseconds: + +```jsonc +{ + "method": "GET", + "url": "https://example.com", + "yes": false, + "timeout": "800ms", + "dodos": 10, + "requests": 1000, + + "params": [ + // A random value will be selected from the list for first "key1" param on each request + // And always "value" for second "key1" param on each request + // e.g. "?key1=value2&key1=value" + { "key1": ["value1", "value2", "value3", "value4"] }, + { "key1": "value" }, + + // A random value will be selected from the list for param "key2" on each request + // e.g. "?key2=value2" + { "key2": ["value1", "value2"] }, + ], + + "headers": [ + // A random value will be selected from the list for first "key1" header on each request + // And always "value" for second "key1" header on each request + // e.g. "key1: value3", "key1: value" + { "key1": ["value1", "value2", "value3", "value4"] }, + { "key1": "value" }, + + // A random value will be selected from the list for header "key2" on each request + // e.g. "key2: value2" + { "key2": ["value1", "value2"] }, + ], + + "cookies": [ + // A random value will be selected from the list for first "key1" cookie on each request + // And always "value" for second "key1" cookie on each request + // e.g. "key1=value4; key1=value" + { "key1": ["value1", "value2", "value3", "value4"] }, + { "key1": "value" }, + + // A random value will be selected from the list for cookie "key2" on each request + // e.g. "key2=value1" + { "key2": ["value1", "value2"] }, + ], + + "body": "body-text", + // OR + // A random body value will be selected from the list for each request + "body": ["body-text1", "body-text2", "body-text3"], + + "proxy": "http://example.com:8080", + // OR + // A random proxy will be selected from the list for each request + "proxy": [ + "http://example.com:8080", + "http://username:password@example.com:8080", + "socks5://example.com:8080", + "socks5h://example.com:8080", + ], +} +``` + +```sh +dodo -f /path/config.json +# OR +dodo -f https://example.com/config.json +``` + +With Docker: + +```sh +docker run --rm -i -v /path/to/config.json:/config.json aykhans/dodo +# OR +docker run --rm -i aykhans/dodo -f https://example.com/config.json +``` + +### 3. Both (CLI & JSON) + +Override the config file arguments with CLI arguments: + +```sh +dodo -f /path/to/config.json -u https://example.com -m GET -d 10 -r 1000 -t 5s +``` + +With Docker: + +```sh +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 + +If `Headers`, `Params`, `Cookies`, `Body`, and `Proxy` fields have multiple values, each request will choose a random value from the list. + +| Parameter | JSON config file | CLI Flag | CLI Short Flag | Type | Description | Default | +| --------------- | ---------------- | ------------ | -------------- | ------------------------------ | --------------------------------------------------------------- | ------- | +| Config file | - | -config-file | -f | String | Path to the local config file or http(s) URL of the config file | - | +| Yes | yes | -yes | -y | Boolean | Answer yes to all questions | false | +| URL | url | -url | -u | String | URL to send the request to | - | +| Method | method | -method | -m | String | HTTP method | GET | +| Requests | requests | -requests | -r | UnsignedInteger | Total number of requests to send | 1000 | +| Dodos (Threads) | dodos | -dodos | -d | UnsignedInteger | Number of dodos (threads) to send requests in parallel | 1 | +| Timeout | timeout | -timeout | -t | Duration | Timeout for canceling each request (milliseconds) | 10000 | +| Params | params | -param | -p | [{String: String OR [String]}] | Request parameters | - | +| Headers | headers | -header | -H | [{String: String OR [String]}] | Request headers | - | +| Cookies | cookies | -cookie | -c | [{String: String OR [String]}] | Request cookies | - | +| Body | body | -body | -b | String OR [String] | Request body or list of request bodies | - | +| Proxy | proxies | -proxy | -x | String OR [String] | Proxy URL or list of proxy URLs | - | diff --git a/config.json b/config.json index b94018a..48fa3d1 100644 --- a/config.json +++ b/config.json @@ -1,31 +1,35 @@ { "method": "GET", "url": "https://example.com", - "no_proxy_check": false, - "timeout": 10000, - "dodos": 1, - "requests": 1, - "params": { - "param-key1": ["param-value1", "param-value2", "param-value3"], - "param-key2": ["param-value1", "param-value2", "param-value3"] - }, - "headers": { - "header-key1": ["header-value1", "header-value2", "header-value3"], - "header-key2": ["header-value2", "header-value2", "header-value3"] - }, - "cookies": { - "cookie-key1": ["cookie-value1", "cookie-value2", "cookie-value3"], - "cookie-key2": ["cookie-value2", "cookie-value2", "cookie-value3"] - }, - "body": ["body1", "body2", "body3"], - "proxies": [ - { - "url": "http://example.com:8080", - "username": "username", - "password": "password" - }, - { - "url": "http://example.com:8080" - } + "yes": false, + "timeout": "5s", + "dodos": 8, + "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-text1", "body-text2", "body-text3"], + + "proxy": [ + "http://example.com:8080", + "http://username:password@example.com:8080", + "socks5://example.com:8080", + "socks5h://example.com:8080" ] -} +} \ No newline at end of file diff --git a/config.yaml b/config.yaml new file mode 100644 index 0000000..54a4492 --- /dev/null +++ b/config.yaml @@ -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" diff --git a/config/cli.go b/config/cli.go new file mode 100644 index 0000000..794a9d3 --- /dev/null +++ b/config/cli.go @@ -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" +} diff --git a/config/config.go b/config/config.go index c28624f..e3fb905 100644 --- a/config/config.go +++ b/config/config.go @@ -1,43 +1,73 @@ package config import ( + "errors" + "fmt" "net/url" "os" + "slices" "strings" "time" - . "github.com/aykhans/dodo/types" + "github.com/aykhans/dodo/types" "github.com/aykhans/dodo/utils" "github.com/jedib0t/go-pretty/v6/table" ) const ( - VERSION string = "0.5.7" - DefaultUserAgent string = "Dodo/" + VERSION - ProxyCheckURL string = "https://www.google.com" - DefaultMethod string = "GET" - DefaultTimeout uint32 = 10000 // Milliseconds (10 seconds) - DefaultDodosCount uint = 1 - DefaultRequestCount uint = 1 - MaxDodosCountForProxies uint = 20 // Max dodos count for proxy check + VERSION string = "0.6.0" + DefaultUserAgent string = "Dodo/" + VERSION + DefaultMethod string = "GET" + DefaultTimeout time.Duration = time.Second * 10 + DefaultDodosCount uint = 1 + DefaultRequestCount uint = 1 + DefaultYes bool = false ) +var SupportedProxySchemes []string = []string{"http", "socks5", "socks5h"} + type RequestConfig struct { - Method string - URL *url.URL - Timeout time.Duration - DodosCount uint - RequestCount uint - Params map[string][]string - Headers map[string][]string - Cookies map[string][]string - Proxies []Proxy - Body []string - Yes bool - NoProxyCheck bool + Method string `json:"method"` + URL url.URL `json:"url"` + Timeout time.Duration `json:"timeout"` + DodosCount uint `json:"dodos"` + RequestCount uint `json:"requests"` + 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:"proxies"` } -func (config *RequestConfig) Print() { +func NewRequestConfig(conf *Config) *RequestConfig { + return &RequestConfig{ + Method: *conf.Method, + URL: conf.URL.URL, + Timeout: conf.Timeout.Duration, + DodosCount: *conf.DodosCount, + RequestCount: *conf.RequestCount, + Yes: *conf.Yes, + Params: conf.Params, + Headers: conf.Headers, + Cookies: conf.Cookies, + Body: conf.Body, + Proxies: conf.Proxies, + } +} + +func (rc *RequestConfig) GetValidDodosCountForRequests() uint { + return min(rc.DodosCount, rc.RequestCount) +} + +func (rc *RequestConfig) GetMaxConns(minConns uint) uint { + maxConns := max( + minConns, rc.GetValidDodosCountForRequests(), + ) + return ((maxConns * 50 / 100) + maxConns) +} + +func (rc *RequestConfig) Print() { t := table.NewWriter() t.SetOutputMirror(os.Stdout) t.SetStyle(table.StyleLight) @@ -56,151 +86,118 @@ func (config *RequestConfig) Print() { WidthMax: 50}, }) - newHeaders := make(map[string][]string) - newHeaders["User-Agent"] = []string{DefaultUserAgent} - for k, v := range config.Headers { - newHeaders[k] = v - } - t.AppendHeader(table.Row{"Request Configuration"}) - t.AppendRow(table.Row{"Method", config.Method}) + t.AppendRow(table.Row{"URL", rc.URL.String()}) t.AppendSeparator() - t.AppendRow(table.Row{"URL", config.URL}) + t.AppendRow(table.Row{"Method", rc.Method}) t.AppendSeparator() - t.AppendRow(table.Row{"Timeout", config.Timeout}) + t.AppendRow(table.Row{"Timeout", rc.Timeout}) t.AppendSeparator() - t.AppendRow(table.Row{"Dodos", config.DodosCount}) + t.AppendRow(table.Row{"Dodos", rc.DodosCount}) t.AppendSeparator() - t.AppendRow(table.Row{"Requests", config.RequestCount}) + t.AppendRow(table.Row{"Requests", rc.RequestCount}) t.AppendSeparator() - t.AppendRow(table.Row{"Params", string(utils.PrettyJSONMarshal(config.Params, 3, "", " "))}) + t.AppendRow(table.Row{"Params", rc.Params.String()}) t.AppendSeparator() - t.AppendRow(table.Row{"Headers", string(utils.PrettyJSONMarshal(newHeaders, 3, "", " "))}) + t.AppendRow(table.Row{"Headers", rc.Headers.String()}) t.AppendSeparator() - t.AppendRow(table.Row{"Cookies", string(utils.PrettyJSONMarshal(config.Cookies, 3, "", " "))}) + t.AppendRow(table.Row{"Cookies", rc.Cookies.String()}) t.AppendSeparator() - t.AppendRow(table.Row{"Proxies", string(utils.PrettyJSONMarshal(config.Proxies, 3, "", " "))}) + t.AppendRow(table.Row{"Proxy", rc.Proxies.String()}) t.AppendSeparator() - t.AppendRow(table.Row{"Proxy Check", !config.NoProxyCheck}) - t.AppendSeparator() - t.AppendRow(table.Row{"Body", string(utils.PrettyJSONMarshal(config.Body, 3, "", " "))}) + t.AppendRow(table.Row{"Body", rc.Body.String()}) t.Render() } -func (config *RequestConfig) GetValidDodosCountForRequests() uint { - return min(config.DodosCount, config.RequestCount) -} - -func (config *RequestConfig) GetValidDodosCountForProxies() uint { - return min(config.DodosCount, uint(len(config.Proxies)), MaxDodosCountForProxies) -} - -func (config *RequestConfig) GetMaxConns(minConns uint) uint { - maxConns := max( - minConns, config.GetValidDodosCountForRequests(), - ) - return ((maxConns * 50 / 100) + maxConns) -} - type Config struct { - Method string `json:"method" validate:"http_method"` // custom validations: http_method - URL string `json:"url" validate:"http_url,required"` - Timeout uint32 `json:"timeout" validate:"gte=1,lte=100000"` - DodosCount uint `json:"dodos" validate:"gte=1"` - RequestCount uint `json:"requests" validation_name:"request-count" validate:"gte=1"` - NoProxyCheck Option[bool] `json:"no_proxy_check"` + Method *string `json:"method"` + URL *types.RequestURL `json:"url"` + Timeout *types.Timeout `json:"timeout"` + DodosCount *uint `json:"dodos"` + RequestCount *uint `json:"requests"` + 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( - method string, - timeout uint32, - dodosCount uint, - requestCount uint, - noProxyCheck Option[bool], -) *Config { - if noProxyCheck == nil { - noProxyCheck = NewNoneOption[bool]() - } - - return &Config{ - Method: method, - Timeout: timeout, - DodosCount: dodosCount, - RequestCount: requestCount, - NoProxyCheck: noProxyCheck, - } +func NewConfig() *Config { + return &Config{} } -func (config *Config) MergeConfigs(newConfig *Config) { - if newConfig.Method != "" { +func (c *Config) Validate() []error { + var errs []error + if utils.IsNilOrZero(c.URL) { + errs = append(errs, errors.New("request URL is required")) + } + 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 } - if newConfig.URL != "" { + if newConfig.URL != nil { config.URL = newConfig.URL } - if newConfig.Timeout != 0 { + if newConfig.Timeout != nil { config.Timeout = newConfig.Timeout } - if newConfig.DodosCount != 0 { + if newConfig.DodosCount != nil { config.DodosCount = newConfig.DodosCount } - if newConfig.RequestCount != 0 { + if newConfig.RequestCount != nil { config.RequestCount = newConfig.RequestCount } - if !newConfig.NoProxyCheck.IsNone() { - config.NoProxyCheck = newConfig.NoProxyCheck + if newConfig.Yes != nil { + 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 { config.Params = newConfig.Params } @@ -218,28 +215,20 @@ func (config *JSONConfig) MergeConfigs(newConfig *JSONConfig) { } } -type CLIConfig struct { - *Config - Yes Option[bool] `json:"yes" validate:"omitempty"` - ConfigFile string `validation_name:"config-file" validate:"omitempty,filepath"` -} - -func NewCLIConfig( - config *Config, - yes Option[bool], - configFile string, -) *CLIConfig { - return &CLIConfig{ - config, yes, configFile, - } -} - -func (config *CLIConfig) MergeConfigs(newConfig *CLIConfig) { - config.Config.MergeConfigs(newConfig.Config) - if newConfig.ConfigFile != "" { - config.ConfigFile = newConfig.ConfigFile - } - if !newConfig.Yes.IsNone() { - config.Yes = newConfig.Yes +func (config *Config) SetDefaults() { + if config.Method == nil { + config.Method = utils.ToPtr(DefaultMethod) + } + if config.Timeout == nil { + config.Timeout = &types.Timeout{Duration: DefaultTimeout} + } + if config.DodosCount == nil { + config.DodosCount = utils.ToPtr(DefaultDodosCount) + } + if config.RequestCount == nil { + config.RequestCount = utils.ToPtr(DefaultRequestCount) + } + if config.Yes == nil { + config.Yes = utils.ToPtr(DefaultYes) } } diff --git a/config/file.go b/config/file.go new file mode 100644 index 0000000..9607390 --- /dev/null +++ b/config/file.go @@ -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 +} diff --git a/custom_errors/errors.go b/custom_errors/errors.go deleted file mode 100644 index 301efa2..0000000 --- a/custom_errors/errors.go +++ /dev/null @@ -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 -} diff --git a/custom_errors/formaters.go b/custom_errors/formaters.go deleted file mode 100644 index 99c5252..0000000 --- a/custom_errors/formaters.go +++ /dev/null @@ -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" -} diff --git a/go.mod b/go.mod index e4a4a9e..cb5efa6 100644 --- a/go.mod +++ b/go.mod @@ -1,29 +1,20 @@ module github.com/aykhans/dodo -go 1.24 +go 1.24.0 require ( - github.com/fatih/color v1.18.0 - github.com/go-playground/validator/v10 v10.25.0 github.com/jedib0t/go-pretty/v6 v6.6.7 github.com/valyala/fasthttp v1.59.0 - golang.org/x/net v0.37.0 ) require ( github.com/andybalholm/brotli v1.1.1 // indirect - github.com/gabriel-vasile/mimetype v1.4.8 // indirect - github.com/go-playground/locales v0.14.1 // indirect - github.com/go-playground/universal-translator v0.18.1 // indirect github.com/klauspost/compress v1.17.11 // indirect - github.com/leodido/go-urn v1.4.0 // indirect - github.com/mattn/go-colorable v0.1.13 // indirect - github.com/mattn/go-isatty v0.0.20 // indirect github.com/mattn/go-runewidth v0.0.16 // indirect github.com/rivo/uniseg v0.4.7 // indirect github.com/valyala/bytebufferpool v1.0.0 // indirect - golang.org/x/crypto v0.36.0 // indirect - golang.org/x/sys v0.31.0 // indirect - golang.org/x/term v0.30.0 // indirect - golang.org/x/text v0.23.0 // indirect + golang.org/x/net v0.35.0 // indirect + golang.org/x/sys v0.30.0 // indirect + golang.org/x/term v0.29.0 // indirect + golang.org/x/text v0.22.0 // indirect ) diff --git a/go.sum b/go.sum index 19ab2ee..730e506 100644 --- a/go.sum +++ b/go.sum @@ -2,29 +2,10 @@ github.com/andybalholm/brotli v1.1.1 h1:PR2pgnyFznKEugtsUo0xLdDop5SKXd5Qf5ysW+7X github.com/andybalholm/brotli v1.1.1/go.mod h1:05ib4cKhjx3OQYUY22hTVd34Bc8upXjOLL2rKwwZBoA= github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c= github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= -github.com/fatih/color v1.18.0 h1:S8gINlzdQ840/4pfAwic/ZE0djQEH3wM94VfqLTZcOM= -github.com/fatih/color v1.18.0/go.mod h1:4FelSpRwEGDpQ12mAdzqdOukCy4u8WUtOY6lkT/6HfU= -github.com/gabriel-vasile/mimetype v1.4.8 h1:FfZ3gj38NjllZIeJAmMhr+qKL8Wu+nOoI3GqacKw1NM= -github.com/gabriel-vasile/mimetype v1.4.8/go.mod h1:ByKUIKGjh1ODkGM1asKUbQZOLGrPjydw3hYPU2YU9t8= -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.25.0 h1:5Dh7cjvzR7BRZadnsVOzPhWsrwUr0nmsZJxEAnFLNO8= -github.com/go-playground/validator/v10 v10.25.0/go.mod h1:GGzBIJMuE98Ic/kJsBXbz1x/7cByt++cQ+YOuDM5wus= github.com/jedib0t/go-pretty/v6 v6.6.7 h1:m+LbHpm0aIAPLzLbMfn8dc3Ht8MW7lsSO4MPItz/Uuo= github.com/jedib0t/go-pretty/v6 v6.6.7/go.mod h1:YwC5CE4fJ1HFUDeivSV1r//AmANFHyqczZk+U6BDALU= github.com/klauspost/compress v1.17.11 h1:In6xLpyWOi1+C7tXUUWv2ot1QvBjxevKAaI6IXrJmUc= github.com/klauspost/compress v1.17.11/go.mod h1:pMDklpSncoRMuLFrf1W9Ss9KT+0rH90U12bZKk7uwG0= -github.com/leodido/go-urn v1.4.0 h1:WT9HwE9SGECu3lg4d/dIA+jxlljEa1/ffXKmRjqdmIQ= -github.com/leodido/go-urn v1.4.0/go.mod h1:bvxc+MVxLKB4z00jd1z+Dvzr47oO32F/QSNjSBOlFxI= -github.com/mattn/go-colorable v0.1.13 h1:fFA4WZxdEF4tXPZVKMLwD8oUnCTTo08duU7wxecdEvA= -github.com/mattn/go-colorable v0.1.13/go.mod h1:7S9/ev0klgBDR4GtXTXX8a3vIGJpMovkB8vQcUbaXHg= -github.com/mattn/go-isatty v0.0.16/go.mod h1:kYGgaQfpe5nmfYZH+SKPsOc2e4SrIfOl2e/yFXSvRLM= -github.com/mattn/go-isatty v0.0.20 h1:xfD0iDuEKnDkl03q4limB+vH+GxLEtL/jb4xVJSWWEY= -github.com/mattn/go-isatty v0.0.20/go.mod h1:W+V8PltTTMOvKvAeJH7IuucS94S2C6jfK/D7dTCTo3Y= github.com/mattn/go-runewidth v0.0.16 h1:E5ScNMtiwvlvB5paMFdw9p4kSQzbXFikJ5SQO6TULQc= github.com/mattn/go-runewidth v0.0.16/go.mod h1:Jdepj2loyihRzMpdS35Xk/zdY8IAYHsh153qUoGf23w= github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM= @@ -40,17 +21,13 @@ github.com/valyala/fasthttp v1.59.0 h1:Qu0qYHfXvPk1mSLNqcFtEk6DpxgA26hy6bmydotDp github.com/valyala/fasthttp v1.59.0/go.mod h1:GTxNb9Bc6r2a9D0TWNSPwDz78UxnTGBViY3xZNEqyYU= github.com/xyproto/randomstring v1.0.5 h1:YtlWPoRdgMu3NZtP45drfy1GKoojuR7hmRcnhZqKjWU= github.com/xyproto/randomstring v1.0.5/go.mod h1:rgmS5DeNXLivK7YprL0pY+lTuhNQW3iGxZ18UQApw/E= -golang.org/x/crypto v0.36.0 h1:AnAEvhDddvBdpY+uR+MyHmuZzzNqXSe/GvuDeob5L34= -golang.org/x/crypto v0.36.0/go.mod h1:Y4J0ReaxCR1IMaabaSMugxJES1EpwhBHhv2bDHklZvc= -golang.org/x/net v0.37.0 h1:1zLorHbz+LYj7MQlSf1+2tPIIgibq2eL5xkrGk6f+2c= -golang.org/x/net v0.37.0/go.mod h1:ivrbrMbzFq5J41QOQh0siUuly180yBYtLp+CKbEaFx8= -golang.org/x/sys v0.0.0-20220811171246-fbc7d0a398ab/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= -golang.org/x/sys v0.6.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= -golang.org/x/sys v0.31.0 h1:ioabZlmFYtWhL+TRYpcnNlLwhyxaM9kWTDEmfnprqik= -golang.org/x/sys v0.31.0/go.mod h1:BJP2sWEmIv4KK5OTEluFJCKSidICx8ciO85XgH3Ak8k= -golang.org/x/term v0.30.0 h1:PQ39fJZ+mfadBm0y5WlL4vlM7Sx1Hgf13sMIY2+QS9Y= -golang.org/x/term v0.30.0/go.mod h1:NYYFdzHoI5wRh/h5tDMdMqCqPJZEuNqVR5xJLd/n67g= -golang.org/x/text v0.23.0 h1:D71I7dUrlY+VX0gQShAThNGHFxZ13dGLBHQLVl1mJlY= -golang.org/x/text v0.23.0/go.mod h1:/BLNzu4aZCJ1+kcD0DNRotWKage4q2rGVAg4o22unh4= +golang.org/x/net v0.35.0 h1:T5GQRQb2y08kTAByq9L4/bz8cipCdA8FbRTXewonqY8= +golang.org/x/net v0.35.0/go.mod h1:EglIi67kWsHKlRzzVMUD93VMSWGFOMSZgxFjparz1Qk= +golang.org/x/sys v0.30.0 h1:QjkSwP/36a20jFYWkSue1YwXzLmsV5Gfq7Eiy72C1uc= +golang.org/x/sys v0.30.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA= +golang.org/x/term v0.29.0 h1:L6pJp37ocefwRRtYPKSWOWzOtWSxVajvz2ldH/xi3iU= +golang.org/x/term v0.29.0/go.mod h1:6bl4lRlvVuDgSf3179VpIxBF0o10JUpXWOnI7nErv7s= +golang.org/x/text v0.22.0 h1:bofq7m3/HAFvbF51jz3Q9wLg3jkvSPuiZu/pD1XwgtM= +golang.org/x/text v0.22.0/go.mod h1:YRoo4H8PVmsu+E3Ou7cqLVH8oXWIHVoX0jqUWALQhfY= gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA= gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= diff --git a/main.go b/main.go index ea79550..5e99a86 100644 --- a/main.go +++ b/main.go @@ -2,118 +2,57 @@ package main import ( "context" + "errors" "fmt" - "net/url" "os" "os/signal" - "strings" "syscall" - "time" "github.com/aykhans/dodo/config" - customerrors "github.com/aykhans/dodo/custom_errors" - "github.com/aykhans/dodo/readers" "github.com/aykhans/dodo/requests" + "github.com/aykhans/dodo/types" "github.com/aykhans/dodo/utils" - "github.com/aykhans/dodo/validation" - "github.com/fatih/color" - goValidator "github.com/go-playground/validator/v10" + "github.com/jedib0t/go-pretty/v6/text" ) func main() { - validator := validation.NewValidator() - conf := config.NewConfig("", 0, 0, 0, nil) - jsonConf := config.NewJSONConfig( - config.NewConfig("", 0, 0, 0, nil), nil, nil, nil, nil, nil, - ) - - cliConf, err := readers.CLIConfigReader() - if err != nil { - utils.PrintAndExit(err.Error()) - } - if cliConf == nil { - os.Exit(0) - } - - if err := validator.StructPartial(cliConf, "ConfigFile"); err != nil { - utils.PrintErrAndExit( - customerrors.ValidationErrorsFormater( - err.(goValidator.ValidationErrors), - ), - ) - } - if cliConf.ConfigFile != "" { - jsonConfNew, err := readers.JSONConfigReader(cliConf.ConfigFile) - if err != nil { - utils.PrintErrAndExit(err) - } - if err := validator.StructFiltered( - jsonConfNew, - func(ns []byte) bool { - return strings.LastIndex(string(ns), "Proxies") == -1 - }); err != nil { - utils.PrintErrAndExit( - customerrors.ValidationErrorsFormater( - err.(goValidator.ValidationErrors), - ), - ) - } - jsonConf = jsonConfNew - conf.MergeConfigs(jsonConf.Config) - } - - conf.MergeConfigs(cliConf.Config) - conf.SetDefaults() - if err := validator.Struct(conf); err != nil { - utils.PrintErrAndExit( - customerrors.ValidationErrorsFormater( - err.(goValidator.ValidationErrors), - ), - ) - } - - parsedURL, err := url.Parse(conf.URL) + conf := config.NewConfig() + configFile, err := conf.ReadCLI() if err != nil { utils.PrintErrAndExit(err) } - requestConf := &config.RequestConfig{ - Method: conf.Method, - URL: parsedURL, - Timeout: time.Duration(conf.Timeout) * time.Millisecond, - DodosCount: conf.DodosCount, - RequestCount: conf.RequestCount, - Params: jsonConf.Params, - Headers: jsonConf.Headers, - Cookies: jsonConf.Cookies, - Proxies: jsonConf.Proxies, - Body: jsonConf.Body, - Yes: cliConf.Yes.ValueOr(false), - NoProxyCheck: conf.NoProxyCheck.ValueOr(false), - } - requestConf.Print() - if !cliConf.Yes.ValueOr(false) { - response := readers.CLIYesOrNoReader("Do you want to continue?", true) - if !response { - utils.PrintAndExit("Exiting...") + + if configFile.String() != "" { + tempConf := config.NewConfig() + if err := tempConf.ReadFile(configFile); err != nil { + utils.PrintErrAndExit(err) + } + tempConf.MergeConfig(conf) + conf = tempConf + } + conf.SetDefaults() + + if errs := conf.Validate(); len(errs) > 0 { + utils.PrintErrAndExit(errors.Join(errs...)) + } + + requestConf := config.NewRequestConfig(conf) + requestConf.Print() + + if !requestConf.Yes { + response := config.CLIYesOrNoReader("Do you want to continue?", false) + if !response { + utils.PrintAndExit("Exiting...\n") } - fmt.Println() } ctx, cancel := context.WithCancel(context.Background()) - sigChan := make(chan os.Signal, 1) - signal.Notify(sigChan, syscall.SIGINT, syscall.SIGTERM) - go func() { - <-sigChan - cancel() - }() + go listenForTermination(func() { cancel() }) responses, err := requests.Run(ctx, requestConf) if err != nil { - if customerrors.Is(err, customerrors.ErrInterrupt) { - color.Yellow(err.Error()) - return - } else if customerrors.Is(err, customerrors.ErrNoInternet) { - utils.PrintAndExit("No internet connection") + if err == types.ErrInterrupt { + fmt.Println(text.FgYellow.Sprint(err.Error())) return } utils.PrintErrAndExit(err) @@ -121,3 +60,10 @@ func main() { responses.Print() } + +func listenForTermination(do func()) { + sigChan := make(chan os.Signal, 1) + signal.Notify(sigChan, syscall.SIGINT, syscall.SIGTERM) + <-sigChan + do() +} diff --git a/readers/cli.go b/readers/cli.go deleted file mode 100644 index 1047013..0000000 --- a/readers/cli.go +++ /dev/null @@ -1,155 +0,0 @@ -package readers - -import ( - "flag" - "fmt" - "strings" - - "github.com/aykhans/dodo/config" - . "github.com/aykhans/dodo/types" - "github.com/fatih/color" -) - -const usageText = `Usage: - dodo [flags] - -Examples: - -Simple usage only with URL: - dodo -u https://example.com - -Simple usage with config file: - dodo -c /path/to/config/file/config.json - -Usage with all flags: - dodo -c /path/to/config/file/config.json -u https://example.com -m POST -d 10 -r 1000 -t 2000 --no-proxy-check -y - -Flags: - -h, --help help for dodo - -v, --version version for dodo - -c, --config-file string Path to the config file - -d, --dodos uint Number of dodos(threads) (default %d) - -m, --method string HTTP Method (default %s) - -r, --request uint Number of total requests (default %d) - -t, --timeout uint32 Timeout for each request in milliseconds (default %d) - -u, --url string URL for stress testing - --no-proxy-check bool Do not check for proxies (default false) - -y, --yes bool Answer yes to all questions (default false)` - -func CLIConfigReader() (*config.CLIConfig, error) { - flag.Usage = func() { - fmt.Printf( - usageText+"\n", - config.DefaultDodosCount, - config.DefaultMethod, - config.DefaultRequestCount, - config.DefaultTimeout, - ) - } - - var ( - cliConfig = config.NewCLIConfig(config.NewConfig("", 0, 0, 0, nil), NewOption(false), "") - configFile = "" - yes = false - method = "" - url = "" - dodosCount uint = 0 - requestsCount uint = 0 - timeout uint = 0 - noProxyCheck bool = false - ) - { - flag.Bool("version", false, "Prints the version of the program") - flag.Bool("v", false, "Prints the version of the program") - - flag.StringVar(&configFile, "config-file", "", "Path to the configuration file") - flag.StringVar(&configFile, "c", "", "Path to the configuration file") - - flag.BoolVar(&yes, "yes", false, "Answer yes to all questions") - flag.BoolVar(&yes, "y", false, "Answer yes to all questions") - - flag.StringVar(&method, "method", "", "HTTP Method") - flag.StringVar(&method, "m", "", "HTTP Method") - - flag.StringVar(&url, "url", "", "URL to send the request") - flag.StringVar(&url, "u", "", "URL to send the request") - - flag.UintVar(&dodosCount, "dodos", 0, "Number of dodos(threads)") - flag.UintVar(&dodosCount, "d", 0, "Number of dodos(threads)") - - flag.UintVar(&requestsCount, "requests", 0, "Number of total requests") - flag.UintVar(&requestsCount, "r", 0, "Number of total requests") - - flag.UintVar(&timeout, "timeout", 0, "Timeout for each request in milliseconds") - flag.UintVar(&timeout, "t", 0, "Timeout for each request in milliseconds") - - flag.BoolVar(&noProxyCheck, "no-proxy-check", false, "Do not check for active proxies") - } - - flag.Parse() - - args := flag.Args() - if len(args) > 0 { - return nil, fmt.Errorf("unexpected arguments: %v", strings.Join(args, ", ")) - } - - returnNil := false - flag.Visit(func(f *flag.Flag) { - switch f.Name { - case "version", "v": - fmt.Printf("dodo version %s\n", config.VERSION) - returnNil = true - case "config-file", "c": - cliConfig.ConfigFile = configFile - case "yes", "y": - cliConfig.Yes.SetValue(yes) - case "method", "m": - cliConfig.Method = method - case "url", "u": - cliConfig.URL = url - case "dodos", "d": - cliConfig.DodosCount = dodosCount - case "requests", "r": - cliConfig.RequestCount = requestsCount - case "timeout", "t": - var maxUint32 uint = 4294967295 - if timeout > maxUint32 { - color.Yellow("timeout value is too large, setting to %d", 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" -} diff --git a/readers/json.go b/readers/json.go deleted file mode 100644 index 06ba65e..0000000 --- a/readers/json.go +++ /dev/null @@ -1,66 +0,0 @@ -package readers - -import ( - "encoding/json" - "fmt" - "net/http" - "os" - "strings" - "time" - - "io" - - "github.com/aykhans/dodo/config" - customerrors "github.com/aykhans/dodo/custom_errors" -) - -func JSONConfigReader(filePath string) (*config.JSONConfig, error) { - var ( - data []byte - err error - ) - - if strings.HasPrefix(filePath, "http://") || strings.HasPrefix(filePath, "https://") { - client := &http.Client{ - Timeout: 10 * time.Second, - } - - resp, err := client.Get(filePath) - if err != nil { - return nil, fmt.Errorf("failed to fetch JSON config from %s", filePath) - } - defer resp.Body.Close() - - data, err = io.ReadAll(io.Reader(resp.Body)) - if err != nil { - return nil, fmt.Errorf("failed to read JSON config from %s", filePath) - } - } else { - 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 -} diff --git a/requests/client.go b/requests/client.go index 3cbd281..a385f78 100644 --- a/requests/client.go +++ b/requests/client.go @@ -2,16 +2,12 @@ package requests import ( "context" - "fmt" + "errors" "math/rand" "net/url" - "sync" "time" - "github.com/aykhans/dodo/config" - "github.com/aykhans/dodo/readers" "github.com/aykhans/dodo/utils" - "github.com/fatih/color" "github.com/valyala/fasthttp" "github.com/valyala/fasthttp/fasthttpproxy" ) @@ -23,71 +19,38 @@ type ClientGeneratorFunc func() *fasthttp.HostClient func getClients( ctx context.Context, timeout time.Duration, - proxies []config.Proxy, - dodosCount uint, + proxies []url.URL, maxConns uint, - yes bool, - noProxyCheck bool, - URL *url.URL, + URL url.URL, ) []*fasthttp.HostClient { isTLS := URL.Scheme == "https" if proxiesLen := len(proxies); proxiesLen > 0 { - // If noProxyCheck is true, we will return the clients without checking the proxies. - if noProxyCheck { - clients := make([]*fasthttp.HostClient, 0, proxiesLen) - addr := URL.Host - if isTLS && URL.Port() == "" { - addr += ":443" - } - - for _, proxy := range proxies { - dialFunc, err := getDialFunc(&proxy, timeout) - if err != nil { - continue - } - - clients = append(clients, &fasthttp.HostClient{ - MaxConns: int(maxConns), - IsTLS: isTLS, - Addr: addr, - Dial: dialFunc, - MaxIdleConnDuration: timeout, - MaxConnDuration: timeout, - WriteTimeout: timeout, - ReadTimeout: timeout, - }, - ) - } - return clients + clients := make([]*fasthttp.HostClient, 0, proxiesLen) + addr := URL.Host + if isTLS && URL.Port() == "" { + addr += ":443" } - // Else, we will check the proxies and return the active ones. - activeProxyClients := getActiveProxyClients( - ctx, proxies, timeout, dodosCount, maxConns, URL, - ) - if ctx.Err() != nil { - return nil - } - activeProxyClientsCount := uint(len(activeProxyClients)) - var yesOrNoMessage string - var yesOrNoDefault bool - if activeProxyClientsCount == 0 { - yesOrNoDefault = false - yesOrNoMessage = color.YellowString("No active proxies found. Do you want to continue?") - } else { - yesOrNoMessage = color.YellowString("Found %d active proxies. Do you want to continue?", activeProxyClientsCount) - } - if !yes { - response := readers.CLIYesOrNoReader("\n"+yesOrNoMessage, yesOrNoDefault) - if !response { - utils.PrintAndExit("Exiting...") + 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, + }, + ) } - fmt.Println() - if activeProxyClientsCount > 0 { - return activeProxyClients - } + return clients } client := &fasthttp.HostClient{ @@ -102,200 +65,19 @@ func getClients( return []*fasthttp.HostClient{client} } -// getActiveProxyClients divides the proxies into slices based on the number of dodos and -// launches goroutines to find active proxy clients for each slice. -// It uses a progress tracker to monitor the progress of the search. -// Once all goroutines have completed, the function waits for them to finish and -// returns a flattened slice of active proxy clients. -func getActiveProxyClients( - ctx context.Context, - proxies []config.Proxy, - timeout time.Duration, - dodosCount uint, - maxConns uint, - URL *url.URL, -) []*fasthttp.HostClient { - activeProxyClientsArray := make([][]*fasthttp.HostClient, dodosCount) - proxiesCount := len(proxies) - dodosCountInt := int(dodosCount) - - var ( - wg sync.WaitGroup - streamWG sync.WaitGroup - ) - wg.Add(dodosCountInt) - streamWG.Add(1) - var proxiesSlice []config.Proxy - increase := make(chan int64, proxiesCount) - - streamCtx, streamCtxCancel := context.WithCancel(context.Background()) - go streamProgress(streamCtx, &streamWG, int64(proxiesCount), "Searching for active proxies🌐", increase) - - for i := range dodosCountInt { - if i+1 == dodosCountInt { - proxiesSlice = proxies[i*proxiesCount/dodosCountInt:] - } else { - proxiesSlice = proxies[i*proxiesCount/dodosCountInt : (i+1)*proxiesCount/dodosCountInt] - } - go findActiveProxyClients( - ctx, - proxiesSlice, - timeout, - &activeProxyClientsArray[i], - increase, - maxConns, - URL, - &wg, - ) - } - wg.Wait() - streamCtxCancel() - streamWG.Wait() - return utils.Flatten(activeProxyClientsArray) -} - -// findActiveProxyClients checks a list of proxies to determine which ones are active -// and appends the active ones to the provided activeProxyClients slice. -// -// Parameters: -// - ctx: The context to control cancellation and timeout. -// - proxies: A slice of Proxy configurations to be checked. -// - timeout: The duration to wait for each proxy check before timing out. -// - activeProxyClients: A pointer to a slice where active proxy clients will be appended. -// - increase: A channel to signal the increase of checked proxies count. -// - URL: The URL to be used for checking the proxies. -// - wg: A WaitGroup to signal when the function is done. -// -// The function sends a GET request to each proxy using the provided URL. If the proxy -// responds with a status code of 200, it is considered active and added to the activeProxyClients slice. -// The function respects the context's cancellation and timeout settings. -func findActiveProxyClients( - ctx context.Context, - proxies []config.Proxy, - timeout time.Duration, - activeProxyClients *[]*fasthttp.HostClient, - increase chan<- int64, - maxConns uint, - URL *url.URL, - wg *sync.WaitGroup, -) { - defer wg.Done() - - request := fasthttp.AcquireRequest() - defer fasthttp.ReleaseRequest(request) - request.SetRequestURI(config.ProxyCheckURL) - request.Header.SetMethod("GET") - - for _, proxy := range proxies { - if ctx.Err() != nil { - return - } - - func() { - defer func() { increase <- 1 }() - - response := fasthttp.AcquireResponse() - defer fasthttp.ReleaseResponse(response) - - dialFunc, err := getDialFunc(&proxy, timeout) - if err != nil { - return - } - client := &fasthttp.Client{ - Dial: dialFunc, - } - defer client.CloseIdleConnections() - - ch := make(chan error) - go func() { - err := client.DoTimeout(request, response, timeout) - ch <- err - }() - select { - case err := <-ch: - if err != nil { - return - } - break - case <-time.After(timeout): - return - case <-ctx.Done(): - return - } - - isTLS := URL.Scheme == "https" - addr := URL.Host - if isTLS && URL.Port() == "" { - addr += ":443" - } - if response.StatusCode() == 200 { - *activeProxyClients = append( - *activeProxyClients, - &fasthttp.HostClient{ - MaxConns: int(maxConns), - IsTLS: isTLS, - Addr: addr, - Dial: dialFunc, - MaxIdleConnDuration: timeout, - MaxConnDuration: timeout, - WriteTimeout: timeout, - ReadTimeout: timeout, - }, - ) - } - }() - } -} - -// getDialFunc returns a fasthttp.DialFunc based on the provided proxy configuration. -// It takes a pointer to a config.Proxy struct as input and returns a fasthttp.DialFunc and an error. -// The function parses the proxy URL, determines the scheme (socks5, socks5h, http, or https), -// and creates a dialer accordingly. If the proxy URL is invalid or the scheme is not supported, -// it returns an error. -func getDialFunc(proxy *config.Proxy, timeout time.Duration) (fasthttp.DialFunc, error) { - parsedProxyURL, err := url.Parse(proxy.URL) - if err != nil { - return nil, err - } - +// getDialFunc returns the appropriate fasthttp.DialFunc based on the provided proxy URL scheme. +// It supports SOCKS5 ('socks5' or 'socks5h') and HTTP ('http') proxy schemes. +// For HTTP proxies, the timeout parameter determines connection timeouts. +// Returns an error if the proxy scheme is unsupported. +func getDialFunc(proxy *url.URL, timeout time.Duration) (fasthttp.DialFunc, error) { var dialer fasthttp.DialFunc - if parsedProxyURL.Scheme == "socks5" || parsedProxyURL.Scheme == "socks5h" { - if proxy.Username != "" { - dialer = fasthttpproxy.FasthttpSocksDialer( - fmt.Sprintf( - "%s://%s:%s@%s", - parsedProxyURL.Scheme, - proxy.Username, - proxy.Password, - parsedProxyURL.Host, - ), - ) - } else { - dialer = fasthttpproxy.FasthttpSocksDialer( - fmt.Sprintf( - "%s://%s", - parsedProxyURL.Scheme, - parsedProxyURL.Host, - ), - ) - } - } else if parsedProxyURL.Scheme == "http" { - if proxy.Username != "" { - dialer = fasthttpproxy.FasthttpHTTPDialerTimeout( - fmt.Sprintf( - "%s:%s@%s", - proxy.Username, proxy.Password, parsedProxyURL.Host, - ), - timeout, - ) - } else { - dialer = fasthttpproxy.FasthttpHTTPDialerTimeout( - parsedProxyURL.Host, - timeout, - ) - } + + if proxy.Scheme == "socks5" || proxy.Scheme == "socks5h" { + dialer = fasthttpproxy.FasthttpSocksDialerDualStack(proxy.String()) + } else if proxy.Scheme == "http" { + dialer = fasthttpproxy.FasthttpHTTPDialerDualStackTimeout(proxy.String(), timeout) } else { - return nil, err + return nil, errors.New("unsupported proxy scheme") } return dialer, nil } diff --git a/requests/helper.go b/requests/helper.go index c8ed9b4..5f9768a 100644 --- a/requests/helper.go +++ b/requests/helper.go @@ -7,7 +7,6 @@ import ( "time" "github.com/jedib0t/go-pretty/v6/progress" - "github.com/valyala/fasthttp" ) // streamProgress streams the progress of a task to the console using a progress bar. @@ -37,9 +36,11 @@ func streamProgress( for { select { case <-ctx.Done(): + if ctx.Err() != context.Canceled { + dodosTracker.MarkAsErrored() + } fmt.Printf("\r") - dodosTracker.MarkAsErrored() - time.Sleep(time.Millisecond * 300) + time.Sleep(time.Millisecond * 500) pw.Stop() 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 - } -} diff --git a/requests/request.go b/requests/request.go index 07a37a6..f65804a 100644 --- a/requests/request.go +++ b/requests/request.go @@ -7,7 +7,7 @@ import ( "time" "github.com/aykhans/dodo/config" - customerrors "github.com/aykhans/dodo/custom_errors" + "github.com/aykhans/dodo/types" "github.com/aykhans/dodo/utils" "github.com/valyala/fasthttp" ) @@ -43,9 +43,9 @@ func (r *Request) Send(ctx context.Context, timeout time.Duration) (*fasthttp.Re return response, nil case <-time.After(timeout): fasthttp.ReleaseResponse(response) - return nil, customerrors.ErrTimeout + return nil, types.ErrTimeout case <-ctx.Done(): - return nil, customerrors.ErrInterrupt + return nil, types.ErrInterrupt } } @@ -74,9 +74,9 @@ func newRequest( getRequest := getRequestGeneratorFunc( requestConfig.URL, + requestConfig.Params, requestConfig.Headers, requestConfig.Cookies, - requestConfig.Params, requestConfig.Method, requestConfig.Body, localRand, @@ -90,37 +90,36 @@ func newRequest( return requests } -// getRequestGeneratorFunc returns a RequestGeneratorFunc which generates HTTP requests -// with the specified parameters. -// The function uses a local random number generator to select bodies, headers, cookies, and parameters -// if multiple options are provided. +// getRequestGeneratorFunc returns a RequestGeneratorFunc which generates HTTP requests with the specified parameters. +// The function uses a local random number generator to select bodies, headers, cookies, and parameters if multiple options are provided. func getRequestGeneratorFunc( - URL *url.URL, - Headers map[string][]string, - Cookies map[string][]string, - Params map[string][]string, - Method string, - Bodies []string, + URL url.URL, + params types.Params, + headers types.Headers, + cookies types.Cookies, + method string, + bodies []string, localRand *rand.Rand, ) RequestGeneratorFunc { - bodiesLen := len(Bodies) + bodiesLen := len(bodies) getBody := func() string { return "" } if bodiesLen == 1 { - getBody = func() string { return Bodies[0] } + getBody = func() string { return bodies[0] } } else if bodiesLen > 1 { - getBody = utils.RandomValueCycle(Bodies, localRand) + getBody = utils.RandomValueCycle(bodies, localRand) } - getHeaders := getKeyValueSetFunc(Headers, localRand) - getCookies := getKeyValueSetFunc(Cookies, localRand) - getParams := getKeyValueSetFunc(Params, localRand) + + getParams := getKeyValueGeneratorFunc(params, localRand) + getHeaders := getKeyValueGeneratorFunc(headers, localRand) + getCookies := getKeyValueGeneratorFunc(cookies, localRand) return func() *fasthttp.Request { return newFasthttpRequest( URL, + getParams(), getHeaders(), getCookies(), - getParams(), - Method, + method, getBody(), ) } @@ -129,12 +128,12 @@ func getRequestGeneratorFunc( // newFasthttpRequest creates a new fasthttp.Request object with the provided parameters. // It sets the request URI, host header, headers, cookies, params, method, and body. func newFasthttpRequest( - URL *url.URL, - Headers map[string]string, - Cookies map[string]string, - Params map[string]string, - Method string, - Body string, + URL url.URL, + params []types.KeyValue[string, string], + headers []types.KeyValue[string, string], + cookies []types.KeyValue[string, string], + method string, + body string, ) *fasthttp.Request { request := fasthttp.AcquireRequest() request.SetRequestURI(URL.Path) @@ -142,12 +141,12 @@ func newFasthttpRequest( // Set the host of the request to the host header // If the host header is not set, the request will fail // If there is host header in the headers, it will be overwritten - request.Header.Set("Host", URL.Host) - setRequestHeaders(request, Headers) - setRequestCookies(request, Cookies) - setRequestParams(request, Params) - setRequestMethod(request, Method) - setRequestBody(request, Body) + request.Header.SetHost(URL.Host) + setRequestParams(request, params) + setRequestHeaders(request, headers) + setRequestCookies(request, cookies) + setRequestMethod(request, method) + setRequestBody(request, body) if URL.Scheme == "https" { request.URI().SetScheme("https") } @@ -155,28 +154,28 @@ func newFasthttpRequest( return request } -// setRequestHeaders sets the headers of the given request with the provided key-value pairs. -func setRequestHeaders(req *fasthttp.Request, headers map[string]string) { - req.Header.Set("User-Agent", config.DefaultUserAgent) - for key, value := range headers { - req.Header.Set(key, value) +// setRequestParams adds the query parameters of the given request based on the provided key-value pairs. +func setRequestParams(req *fasthttp.Request, params []types.KeyValue[string, string]) { + for _, param := range params { + req.URI().QueryArgs().Add(param.Key, param.Value) } } -// setRequestCookies sets the cookies in the given request. -func setRequestCookies(req *fasthttp.Request, cookies map[string]string) { - for key, value := range cookies { - req.Header.SetCookie(key, value) +// setRequestHeaders adds the headers of the given request with the provided key-value pairs. +func setRequestHeaders(req *fasthttp.Request, headers []types.KeyValue[string, string]) { + for _, header := range headers { + req.Header.Add(header.Key, header.Value) + } + 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. -func setRequestParams(req *fasthttp.Request, params map[string]string) { - urlParams := url.Values{} - for key, value := range params { - urlParams.Add(key, value) +// setRequestCookies adds the cookies of the given request with the provided key-value pairs. +func setRequestCookies(req *fasthttp.Request, cookies []types.KeyValue[string, string]) { + for _, cookie := range cookies { + req.Header.Add("Cookie", cookie.Key+"="+cookie.Value) } - req.URI().SetQueryString(urlParams.Encode()) } // setRequestMethod sets the HTTP request method for the given request. @@ -190,59 +189,62 @@ func setRequestBody(req *fasthttp.Request, body string) { req.SetBody([]byte(body)) } -// getKeyValueSetFunc generates a function that returns a map of key-value pairs based on the provided key-value set. -// The generated function will either return fixed values or random values depending on the input. +// getKeyValueGeneratorFunc creates a function that generates key-value pairs for HTTP requests. +// It takes a slice of key-value pairs where each key maps to a slice of possible values, +// and a random number generator. // -// Returns: -// - A function that returns a map of key-value pairs. If the input map contains multiple values for a key, -// the returned function will generate random values for that key. If the input map contains a single value -// for a key, the returned function will always return that value. If the input map is empty for a key, -// the returned function will generate an empty string for that key. -func getKeyValueSetFunc[ - KeyValueSet map[string][]string, - KeyValue map[string]string, -](keyValueSet KeyValueSet, localRand *rand.Rand) func() KeyValue { +// If any key has multiple possible values, the function will randomly select one value for each +// call (using the provided random number generator). If all keys have at most one value, the +// function will always return the same set of key-value pairs for efficiency. +func getKeyValueGeneratorFunc[ + T []types.KeyValue[string, string], +]( + keyValueSlice []types.KeyValue[string, []string], + localRand *rand.Rand, +) func() T { getKeyValueSlice := []map[string]func() string{} isRandom := false - for key, values := range keyValueSet { - valuesLen := len(values) - // if values is empty, return a function that generates empty string - // if values has only one element, return a function that generates that element - // if values has more than one element, return a function that generates a random element - getKeyValue := func() string { return "" } + for _, kv := range keyValueSlice { + valuesLen := len(kv.Value) + + getValueFunc := func() string { return "" } if valuesLen == 1 { - getKeyValue = func() string { return values[0] } + getValueFunc = func() string { return kv.Value[0] } } else if valuesLen > 1 { - getKeyValue = utils.RandomValueCycle(values, localRand) + getValueFunc = utils.RandomValueCycle(kv.Value, localRand) isRandom = true } getKeyValueSlice = append( getKeyValueSlice, - map[string]func() string{key: getKeyValue}, + map[string]func() string{kv.Key: getValueFunc}, ) } - // if isRandom is true, return a function that generates random values, - // otherwise return a function that generates fixed values to avoid unnecessary random number generation if isRandom { - return func() KeyValue { - keyValues := make(KeyValue, len(getKeyValueSlice)) - for _, keyValue := range getKeyValueSlice { + return func() T { + keyValues := make(T, len(getKeyValueSlice)) + for i, keyValue := range getKeyValueSlice { for key, value := range keyValue { - keyValues[key] = value() + keyValues[i] = types.KeyValue[string, string]{ + Key: key, + Value: value(), + } } } return keyValues } } else { - keyValues := make(KeyValue, len(getKeyValueSlice)) - for _, keyValue := range getKeyValueSlice { + keyValues := make(T, len(getKeyValueSlice)) + for i, keyValue := range getKeyValueSlice { for key, value := range keyValue { - keyValues[key] = value() + keyValues[i] = types.KeyValue[string, string]{ + Key: key, + Value: value(), + } } } - return func() KeyValue { return keyValues } + return func() T { return keyValues } } } diff --git a/requests/response.go b/requests/response.go index 9bf671d..16155ce 100644 --- a/requests/response.go +++ b/requests/response.go @@ -4,7 +4,7 @@ import ( "os" "time" - . "github.com/aykhans/dodo/types" + "github.com/aykhans/dodo/types" "github.com/aykhans/dodo/utils" "github.com/jedib0t/go-pretty/v6/table" ) @@ -32,8 +32,8 @@ func (responses Responses) Print() { Min: responses[0].Time, Max: responses[0].Time, } - mergedResponses := make(map[string]Durations) - var allDurations Durations + mergedResponses := make(map[string]types.Durations) + var allDurations types.Durations for _, response := range responses { if response.Time < total.Min { diff --git a/requests/run.go b/requests/run.go index 01d9863..c864b1f 100644 --- a/requests/run.go +++ b/requests/run.go @@ -7,48 +7,33 @@ import ( "time" "github.com/aykhans/dodo/config" - customerrors "github.com/aykhans/dodo/custom_errors" + "github.com/aykhans/dodo/types" "github.com/aykhans/dodo/utils" "github.com/valyala/fasthttp" ) // Run executes the main logic for processing requests based on the provided configuration. -// It first checks for an internet connection with a timeout context. If no connection is found, -// it returns an error. Then, it initializes clients based on the request configuration and -// releases the dodos. If the context is canceled and no responses are collected, it returns an interrupt error. +// It initializes clients based on the request configuration and releases the dodos. +// If the context is canceled and no responses are collected, it returns an interrupt error. // // Parameters: // - ctx: The context for managing request lifecycle and cancellation. // - requestConfig: The configuration for the request, including timeout, proxies, and other settings. -// -// Returns: -// - Responses: A collection of responses from the executed requests. -// - error: An error if the operation fails, such as no internet connection or an interrupt. func Run(ctx context.Context, requestConfig *config.RequestConfig) (Responses, error) { - checkConnectionCtx, checkConnectionCtxCancel := context.WithTimeout(ctx, 8*time.Second) - if !checkConnection(checkConnectionCtx) { - checkConnectionCtxCancel() - return nil, customerrors.ErrNoInternet - } - checkConnectionCtxCancel() - clients := getClients( ctx, requestConfig.Timeout, requestConfig.Proxies, - requestConfig.GetValidDodosCountForProxies(), requestConfig.GetMaxConns(fasthttp.DefaultMaxConnsPerHost), - requestConfig.Yes, - requestConfig.NoProxyCheck, requestConfig.URL, ) if clients == nil { - return nil, customerrors.ErrInterrupt + return nil, types.ErrInterrupt } responses := releaseDodos(ctx, requestConfig, clients) if ctx.Err() != nil && len(responses) == 0 { - return nil, customerrors.ErrInterrupt + return nil, types.ErrInterrupt } return responses, nil @@ -139,7 +124,7 @@ func sendRequest( } if err != nil { - if err == customerrors.ErrInterrupt { + if err == types.ErrInterrupt { return } *responseData = append(*responseData, &Response{ diff --git a/types/body.go b/types/body.go new file mode 100644 index 0000000..18ce972 --- /dev/null +++ b/types/body.go @@ -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 +} diff --git a/types/config_file.go b/types/config_file.go new file mode 100644 index 0000000..66d8846 --- /dev/null +++ b/types/config_file.go @@ -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 +} diff --git a/types/cookies.go b/types/cookies.go new file mode 100644 index 0000000..ecc3a62 --- /dev/null +++ b/types/cookies.go @@ -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 { + 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 string, value string) { + if existingValue := cookies.GetValue(key); existingValue != nil { + *cookies = append(*cookies, KeyValue[string, []string]{Key: key, Value: append(existingValue, value)}) + } else { + *cookies = append(*cookies, KeyValue[string, []string]{Key: key, Value: []string{value}}) + } +} + +func (cookies *Cookies) GetValue(key string) []string { + for _, cookie := range *cookies { + if cookie.Key == key { + return cookie.Value + } + } + return nil +} diff --git a/types/duration.go b/types/duration.go new file mode 100644 index 0000000..64c320e --- /dev/null +++ b/types/duration.go @@ -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()) +} diff --git a/types/errors.go b/types/errors.go new file mode 100644 index 0000000..aeb7172 --- /dev/null +++ b/types/errors.go @@ -0,0 +1,10 @@ +package types + +import ( + "errors" +) + +var ( + ErrInterrupt = errors.New("interrupted") + ErrTimeout = errors.New("timeout") +) diff --git a/types/headers.go b/types/headers.go new file mode 100644 index 0000000..c266dac --- /dev/null +++ b/types/headers.go @@ -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 { + 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 string, value string) { + if existingValue := headers.GetValue(key); existingValue != nil { + *headers = append(*headers, KeyValue[string, []string]{Key: key, Value: append(existingValue, value)}) + } else { + *headers = append(*headers, KeyValue[string, []string]{Key: key, Value: []string{value}}) + } +} + +func (headers *Headers) GetValue(key string) []string { + for _, header := range *headers { + if header.Key == key { + return header.Value + } + } + return nil +} diff --git a/types/key_value.go b/types/key_value.go new file mode 100644 index 0000000..1361c35 --- /dev/null +++ b/types/key_value.go @@ -0,0 +1,6 @@ +package types + +type KeyValue[K comparable, V any] struct { + Key K + Value V +} diff --git a/types/option.go b/types/option.go deleted file mode 100644 index c642651..0000000 --- a/types/option.go +++ /dev/null @@ -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} -} diff --git a/types/params.go b/types/params.go new file mode 100644 index 0000000..9b9960e --- /dev/null +++ b/types/params.go @@ -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 { + 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 string, value string) { + if existingValue := params.GetValue(key); existingValue != nil { + *params = append(*params, KeyValue[string, []string]{Key: key, Value: append(existingValue, value)}) + } else { + *params = append(*params, KeyValue[string, []string]{Key: key, Value: []string{value}}) + } +} + +func (params *Params) GetValue(key string) []string { + for _, param := range *params { + if param.Key == key { + return param.Value + } + } + return nil +} diff --git a/types/proxies.go b/types/proxies.go new file mode 100644 index 0000000..ab9f0f8 --- /dev/null +++ b/types/proxies.go @@ -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 +} diff --git a/types/request_url.go b/types/request_url.go new file mode 100644 index 0000000..2fab9d3 --- /dev/null +++ b/types/request_url.go @@ -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 +} diff --git a/utils/compare.go b/utils/compare.go new file mode 100644 index 0000000..6cd12f3 --- /dev/null +++ b/utils/compare.go @@ -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 +} diff --git a/utils/convert.go b/utils/convert.go index 21e7a92..14d369b 100644 --- a/utils/convert.go +++ b/utils/convert.go @@ -1,85 +1,5 @@ package utils -import ( - "encoding/json" - "fmt" - "reflect" -) - -type TruncatedMarshaller struct { - Value interface{} - MaxItems int -} - -func (t TruncatedMarshaller) MarshalJSON() ([]byte, error) { - val := reflect.ValueOf(t.Value) - - if val.Kind() != reflect.Slice && val.Kind() != reflect.Array { - return json.Marshal(t.Value) - } - if val.Len() == 0 { - return []byte("[]"), nil - } - - length := val.Len() - if length <= t.MaxItems { - return json.Marshal(t.Value) - } - - truncated := make([]interface{}, t.MaxItems+1) - - for i := 0; i < t.MaxItems; i++ { - truncated[i] = val.Index(i).Interface() - } - - remaining := length - t.MaxItems - truncated[t.MaxItems] = fmt.Sprintf("+%d", remaining) - - return json.Marshal(truncated) -} - -func PrettyJSONMarshal(v interface{}, maxItems int, prefix, indent string) []byte { - truncated := processValue(v, maxItems) - d, _ := json.MarshalIndent(truncated, prefix, indent) - return d -} - -func processValue(v interface{}, maxItems int) interface{} { - val := reflect.ValueOf(v) - - switch val.Kind() { - case reflect.Map: - newMap := make(map[string]interface{}) - iter := val.MapRange() - for iter.Next() { - k := iter.Key().String() - newMap[k] = processValue(iter.Value().Interface(), maxItems) - } - return newMap - - case reflect.Slice, reflect.Array: - return TruncatedMarshaller{Value: v, MaxItems: maxItems} - - case reflect.Struct: - newMap := make(map[string]interface{}) - t := val.Type() - for i := 0; i < t.NumField(); i++ { - field := t.Field(i) - if field.IsExported() { - jsonTag := field.Tag.Get("json") - if jsonTag == "-" { - continue - } - fieldName := field.Name - if jsonTag != "" { - fieldName = jsonTag - } - newMap[fieldName] = processValue(val.Field(i).Interface(), maxItems) - } - } - return newMap - - default: - return v - } +func ToPtr[T any](value T) *T { + return &value } diff --git a/utils/print.go b/utils/print.go index b4c9e00..489688a 100644 --- a/utils/print.go +++ b/utils/print.go @@ -4,11 +4,11 @@ import ( "fmt" "os" - "github.com/fatih/color" + "github.com/jedib0t/go-pretty/v6/text" ) func PrintErr(err error) { - color.New(color.FgRed).Fprintln(os.Stderr, err.Error()) + fmt.Fprintln(os.Stderr, text.FgRed.Sprint(err.Error())) } func PrintErrAndExit(err error) { diff --git a/utils/slice.go b/utils/slice.go index e323663..55db732 100644 --- a/utils/slice.go +++ b/utils/slice.go @@ -10,15 +10,6 @@ func Flatten[T any](nested [][]*T) []*T { return flattened } -func Contains[T comparable](slice []T, item T) bool { - for _, i := range slice { - if i == item { - return true - } - } - return false -} - // RandomValueCycle returns a function that cycles through the provided slice of values // in a random order. Each call to the returned function will yield a value from the slice. // The order of values is determined by the provided random number generator. diff --git a/validation/validator.go b/validation/validator.go deleted file mode 100644 index 948c48d..0000000 --- a/validation/validator.go +++ /dev/null @@ -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 -}