Compare commits

...

135 Commits
v0.5.2 ... main

Author SHA1 Message Date
cb8898d20e
Merge pull request #108 from aykhans/core/refactor-duration-logic
All checks were successful
golangci-lint / lint (push) Successful in 3m4s
🔨 Replace 'time.AfterFunc' with 'context.WithTimeout'
2025-04-18 15:42:21 +04:00
a552d1c9f9 🔨 Replace 'time.AfterFunc' with 'context.WithTimeout' 2025-04-18 15:39:17 +04:00
35263f1dd6
Merge pull request #106 from aykhans/bump/version
All checks were successful
golangci-lint / lint (push) Successful in 3m0s
⬆️ bump version to 0.6.3
2025-04-03 05:25:06 +04:00
930e173a6a ⬆️ bump version to 0.6.3 2025-04-03 05:24:11 +04:00
bea2a81afa
Merge pull request #104 from aykhans/chore/refactor-ci
🔧 Refactor .golangci and Taskfile
2025-04-03 05:05:50 +04:00
53ed486b23
Merge pull request #105 from aykhans/dependabot/go_modules/github.com/valyala/fasthttp-1.60.0
Bump github.com/valyala/fasthttp from 1.59.0 to 1.60.0
2025-04-03 05:05:02 +04:00
0b9c32a09d
Merge branch 'main' into chore/refactor-ci 2025-04-03 05:04:12 +04:00
42d5617e3f 🎨 Format files 2025-04-03 05:01:22 +04:00
e80ae9ab24 🔧 bump golangci version 2025-04-03 04:24:23 +04:00
dependabot[bot]
86a6f7814b
Bump github.com/valyala/fasthttp from 1.59.0 to 1.60.0
Bumps [github.com/valyala/fasthttp](https://github.com/valyala/fasthttp) from 1.59.0 to 1.60.0.
- [Release notes](https://github.com/valyala/fasthttp/releases)
- [Commits](https://github.com/valyala/fasthttp/compare/v1.59.0...v1.60.0)

---
updated-dependencies:
- dependency-name: github.com/valyala/fasthttp
  dependency-version: 1.60.0
  dependency-type: direct:production
  update-type: version-update:semver-minor
...

Signed-off-by: dependabot[bot] <support@github.com>
2025-04-03 00:20:47 +00:00
09034b5f9e 🔧 bump golangci/golangci-lint-action version 2025-04-03 04:18:53 +04:00
f1ca2041c3 🔧 bump golangci version 2025-04-03 04:15:48 +04:00
f5a29a2657
Merge pull request #103 from aykhans/chore/general-refactoring
General refactoring
2025-04-03 04:11:38 +04:00
439f66eb87 🔧 Refactor .golangci and Taskfile 2025-04-03 03:07:38 +04:00
415d0130ce 🔨 Move duration cancel logic to requests package 2025-04-01 21:10:02 +04:00
abaa8e90b2 🔧 Add 'run' and 'fmt' commands to Taskfile 2025-04-01 21:09:17 +04:00
046ce74cd9
Merge pull request #102 from aykhans/chore/replace-makefile-with-taskfile
All checks were successful
golangci-lint / lint (push) Successful in 2m36s
🔧 Replace Makefile with Taskfile and remove 'build.sh'
2025-04-01 20:01:35 +04:00
681cafc213 🔧 Replace Makefile with Taskfile and remove 'build.sh' 2025-03-27 23:32:41 +04:00
7e05cf4f6b
Merge pull request #101 from aykhans/feat/add-duration
All checks were successful
golangci-lint / lint (push) Successful in 23s
 Add duration
2025-03-24 18:12:10 +04:00
934cd0ad33 📚 Update docs 2025-03-24 17:02:58 +04:00
69c4841a05 📚 Update docs 2025-03-24 17:02:06 +04:00
3cc165cbf4 ⬆️ bump version to 0.6.2 2025-03-24 16:55:23 +04:00
59f40ad825 Add duration 2025-03-24 16:54:09 +04:00
a170588574
Merge pull request #100 from aykhans/refactor/config
All checks were successful
golangci-lint / lint (push) Successful in 25s
🔨 Add 'User-Agent' in 'SetDefaults' function
2025-03-23 21:19:31 +04:00
2a0ac390d8 🔨 Add 'User-Agent'in 'SetDefaults' function 2025-03-22 22:25:54 +04:00
11bb8b3fb0
Merge pull request #99 from aykhans/docs/update-readme
All checks were successful
golangci-lint / lint (push) Successful in 21s
📚 Update README.md
2025-03-20 19:23:41 +04:00
1aadc3419a
📚 Update README.md 2025-03-20 19:23:14 +04:00
b3af3f6ad5
Merge pull request #98 from aykhans/feat/config-yaml
All checks were successful
golangci-lint / lint (push) Successful in 1m15s
 Add yaml file reader to config
2025-03-20 16:38:13 +04:00
ed52fff363 ⬆️ bump version to 0.6.1 2025-03-20 16:31:39 +04:00
985fc6200d 🐳 Update Dockerfile 2025-03-20 16:11:43 +04:00
1808865358 📚 Update docs 2025-03-20 16:01:53 +04:00
56342e49c6 📚 Update docs 2025-03-20 15:59:46 +04:00
ec80569d5d 🐳 Remove default config file path from 'ENTRYPOINT' 2025-03-20 15:59:09 +04:00
459f7ee0dc Add yaml file reader to config 2025-03-20 03:52:25 +04:00
3cd72855e5
Merge pull request #97 from aykhans/fix/nil-url
All checks were successful
golangci-lint / lint (push) Successful in 58s
🐛 Fix nil URL referance
2025-03-20 03:20:33 +04:00
b8011ce651 🐛 Fix nil URL referance 2025-03-20 03:19:28 +04:00
0aeeb484e2
Merge pull request #96 from aykhans/refactor/general
All checks were successful
golangci-lint / lint (push) Successful in 3m58s
💄 General refactoring
2025-03-19 05:29:13 +04:00
fc3244dc33 💄 General refactoring 2025-03-19 05:28:14 +04:00
aa6ec450b8
Merge pull request #95 from aykhans/fix/types
🐛 Fix 'AppendByKey' method of the '[]KeyValue[string, []string]' types
2025-03-19 04:06:47 +04:00
e31f5ad204 🐛 Fix 'AppendByKey' method of the '[]KeyValue[string, []string]' types 2025-03-19 04:06:10 +04:00
de9a4bb355
Merge pull request #94 from aykhans/dependabot/go_modules/go_modules-c153b83258
Bump golang.org/x/net from 0.35.0 to 0.36.0 in the go_modules group
2025-03-19 02:31:20 +04:00
dependabot[bot]
234ca01e41
Bump golang.org/x/net from 0.35.0 to 0.36.0 in the go_modules group
Bumps the go_modules group with 1 update: [golang.org/x/net](https://github.com/golang/net).


Updates `golang.org/x/net` from 0.35.0 to 0.36.0
- [Commits](https://github.com/golang/net/compare/v0.35.0...v0.36.0)

---
updated-dependencies:
- dependency-name: golang.org/x/net
  dependency-type: indirect
  dependency-group: go_modules
...

Signed-off-by: dependabot[bot] <support@github.com>
2025-03-18 22:29:02 +00:00
cc490143ea
Merge pull request #92 from aykhans/refactor/config
Restructure entire project logic
2025-03-19 02:28:13 +04:00
a8c3efe198 🔨 Refactor 'releaseDodos' function 2025-03-19 01:18:36 +04:00
3c2a0ee1b2 📚 Update README.md 2025-03-18 23:41:20 +04:00
00f0bcb2de 🔨 Restructure entire project logic
- 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
2025-03-16 21:20:33 +04:00
8f811e1bec 💄 Format '.golangci.yml' file 2025-03-11 00:25:32 +04:00
58ea31683b
Merge pull request #85 from aykhans/feat/config-file-http-support
Some checks failed
golangci-lint / lint (push) Failing after 44s
 Added http support to 'JSONConfigReader' function
2025-03-09 17:07:41 +04:00
cc2a6eb367 Added http support to 'JSONConfigReader' function 2025-03-09 05:00:33 +04:00
f721abb583
Merge pull request #82 from aykhans/dependabot/go_modules/golang.org/x/net-0.37.0
Some checks failed
golangci-lint / lint (push) Failing after 36s
Bump golang.org/x/net from 0.35.0 to 0.37.0
2025-03-06 15:14:08 +04:00
dependabot[bot]
4a9fb9fdda
Bump golang.org/x/net from 0.35.0 to 0.37.0
Bumps [golang.org/x/net](https://github.com/golang/net) from 0.35.0 to 0.37.0.
- [Commits](https://github.com/golang/net/compare/v0.35.0...v0.37.0)

---
updated-dependencies:
- dependency-name: golang.org/x/net
  dependency-type: direct:production
  update-type: version-update:semver-minor
...

Signed-off-by: dependabot[bot] <support@github.com>
2025-03-06 00:50:05 +00:00
198b6c785a
Merge pull request #81 from aykhans/dependabot/go_modules/github.com/jedib0t/go-pretty/v6-6.6.7
Some checks failed
golangci-lint / lint (push) Failing after 37s
Bump github.com/jedib0t/go-pretty/v6 from 6.6.6 to 6.6.7
2025-03-03 16:17:20 +04:00
dependabot[bot]
9dc56709a7
Bump github.com/jedib0t/go-pretty/v6 from 6.6.6 to 6.6.7
Bumps [github.com/jedib0t/go-pretty/v6](https://github.com/jedib0t/go-pretty) from 6.6.6 to 6.6.7.
- [Release notes](https://github.com/jedib0t/go-pretty/releases)
- [Commits](https://github.com/jedib0t/go-pretty/compare/v6.6.6...v6.6.7)

---
updated-dependencies:
- dependency-name: github.com/jedib0t/go-pretty/v6
  dependency-type: direct:production
  update-type: version-update:semver-patch
...

Signed-off-by: dependabot[bot] <support@github.com>
2025-03-03 01:00:54 +00:00
a9021bd1a4
Merge pull request #80 from aykhans/refactor/color-utils
Some checks failed
golangci-lint / lint (push) Failing after 42s
🔨 Replace color utils with 'github.com/fatih/color'
2025-03-02 20:22:43 +04:00
48c2dc7935 🔖 bump version to 0.5.7 2025-03-02 20:20:06 +04:00
4cb0540824 🔨 Replace color utils with 'github.com/fatih/color' 2025-03-02 20:18:11 +04:00
a01bf19986
Merge pull request #79 from aykhans/bump/go-version
Some checks failed
golangci-lint / lint (push) Failing after 39s
🔖 Bump go version to 1.24
2025-03-01 17:20:56 +04:00
74dcecc8b1 🔖 Bump go version to 1.24 2025-03-01 17:18:59 +04:00
00afca7139
Merge pull request #77 from aykhans/feat/add-makefile
Some checks failed
golangci-lint / lint (push) Failing after 2m11s
🔧 Added Makefile
2025-02-28 03:43:33 +04:00
1589fefeb8 🔧 Added Makefile 2025-02-28 03:41:33 +04:00
bbc43bbaac
Merge pull request #75 from aykhans/ci/add-golangci-lint
Added golangci lint to CI
2025-02-28 03:27:14 +04:00
89fcf5f174 Merge branch 'ci/add-golangci-lint' of https://github.com/aykhans/dodo into ci/add-golangci-lint 2025-02-28 03:25:29 +04:00
a58f734a55 Merge branch 'main' into ci/add-golangci-lint 2025-02-28 03:23:06 +04:00
efd5176ab9
Merge pull request #76 from aykhans/refactor/general-refactoring
🔨 Remove unnecessary conversions
2025-02-28 03:21:26 +04:00
f0adcaf328
👷 Update golangci-lint.yml 2025-02-28 03:18:50 +04:00
6314cf7724 🐳 '.golangci.yml' added to .dockerignore 2025-02-28 03:16:03 +04:00
140e570b85 👷 Added golangci-lint 2025-02-28 03:15:33 +04:00
83c5788e54 🔨 Remove unnecessary conversions 2025-02-28 02:16:04 +04:00
ca74092615
Merge pull request #74 from aykhans/dependabot/go_modules/github.com/valyala/fasthttp-1.59.0
Bump github.com/valyala/fasthttp from 1.58.0 to 1.59.0
2025-02-20 20:41:41 +04:00
dependabot[bot]
004b10ea3c
Bump github.com/valyala/fasthttp from 1.58.0 to 1.59.0
Bumps [github.com/valyala/fasthttp](https://github.com/valyala/fasthttp) from 1.58.0 to 1.59.0.
- [Release notes](https://github.com/valyala/fasthttp/releases)
- [Commits](https://github.com/valyala/fasthttp/compare/v1.58.0...v1.59.0)

---
updated-dependencies:
- dependency-name: github.com/valyala/fasthttp
  dependency-type: direct:production
  update-type: version-update:semver-minor
...

Signed-off-by: dependabot[bot] <support@github.com>
2025-02-20 00:53:09 +00:00
4c9ceb1c4b
Merge pull request #73 from aykhans/dependabot/go_modules/github.com/go-playground/validator/v10-10.25.0
Bump github.com/go-playground/validator/v10 from 10.24.0 to 10.25.0
2025-02-17 15:17:27 +04:00
dependabot[bot]
6f3df7c45b
Bump github.com/go-playground/validator/v10 from 10.24.0 to 10.25.0
Bumps [github.com/go-playground/validator/v10](https://github.com/go-playground/validator) from 10.24.0 to 10.25.0.
- [Release notes](https://github.com/go-playground/validator/releases)
- [Commits](https://github.com/go-playground/validator/compare/v10.24.0...v10.25.0)

---
updated-dependencies:
- dependency-name: github.com/go-playground/validator/v10
  dependency-type: direct:production
  update-type: version-update:semver-minor
...

Signed-off-by: dependabot[bot] <support@github.com>
2025-02-17 00:34:54 +00:00
8cd495055d
Merge pull request #71 from aykhans/dependabot/go_modules/github.com/jedib0t/go-pretty/v6-6.6.6
Bump github.com/jedib0t/go-pretty/v6 from 6.6.5 to 6.6.6
2025-02-11 19:45:45 +04:00
dependabot[bot]
391a5bc6ec
Bump github.com/jedib0t/go-pretty/v6 from 6.6.5 to 6.6.6
Bumps [github.com/jedib0t/go-pretty/v6](https://github.com/jedib0t/go-pretty) from 6.6.5 to 6.6.6.
- [Release notes](https://github.com/jedib0t/go-pretty/releases)
- [Commits](https://github.com/jedib0t/go-pretty/compare/v6.6.5...v6.6.6)

---
updated-dependencies:
- dependency-name: github.com/jedib0t/go-pretty/v6
  dependency-type: direct:production
  update-type: version-update:semver-patch
...

Signed-off-by: dependabot[bot] <support@github.com>
2025-02-11 15:44:47 +00:00
a69e248f8c
Merge pull request #72 from aykhans/dependabot/go_modules/golang.org/x/net-0.35.0
Bump golang.org/x/net from 0.34.0 to 0.35.0
2025-02-11 19:43:31 +04:00
dependabot[bot]
2634ca110c
Bump golang.org/x/net from 0.34.0 to 0.35.0
Bumps [golang.org/x/net](https://github.com/golang/net) from 0.34.0 to 0.35.0.
- [Commits](https://github.com/golang/net/compare/v0.34.0...v0.35.0)

---
updated-dependencies:
- dependency-name: golang.org/x/net
  dependency-type: direct:production
  update-type: version-update:semver-minor
...

Signed-off-by: dependabot[bot] <support@github.com>
2025-02-11 00:48:51 +00:00
b1612598c4
Merge pull request #70 from aykhans/docs/update
Docs/update
2025-01-21 20:53:30 +04:00
ba79304b04
📚 Update README.md 2025-01-21 20:52:25 +04:00
015cb15053 📚 Update docs 2025-01-21 20:50:10 +04:00
3dc002188e 📚 Update docs 2025-01-21 20:47:01 +04:00
769c04685a
Merge pull request #69 from aykhans/fix/connections-bottleneck
🐛 Remove 'CloseIdleConnections' from 'Send' function
2025-01-15 21:44:02 +04:00
e43378a9a4
🔖 bump version to 0.5.6 2025-01-15 21:43:46 +04:00
e29e4f1bc6 🐛 Remove 'CloseIdleConnections' from 'Send' function 2025-01-14 20:43:45 +04:00
7d2168a014
Merge pull request #67 from aykhans/dependabot/go_modules/github.com/go-playground/validator/v10-10.24.0
Bump github.com/go-playground/validator/v10 from 10.23.0 to 10.24.0
2025-01-14 16:29:25 +04:00
dependabot[bot]
ba53e6b7f7
Bump github.com/go-playground/validator/v10 from 10.23.0 to 10.24.0
Bumps [github.com/go-playground/validator/v10](https://github.com/go-playground/validator) from 10.23.0 to 10.24.0.
- [Release notes](https://github.com/go-playground/validator/releases)
- [Commits](https://github.com/go-playground/validator/compare/v10.23.0...v10.24.0)

---
updated-dependencies:
- dependency-name: github.com/go-playground/validator/v10
  dependency-type: direct:production
  update-type: version-update:semver-minor
...

Signed-off-by: dependabot[bot] <support@github.com>
2025-01-14 00:26:46 +00:00
58964e1098
Merge pull request #66 from aykhans/dependabot/go_modules/golang.org/x/net-0.34.0
Bump golang.org/x/net from 0.33.0 to 0.34.0
2025-01-08 15:27:52 +04:00
dependabot[bot]
d4bf7358ff
Bump golang.org/x/net from 0.33.0 to 0.34.0
Bumps [golang.org/x/net](https://github.com/golang/net) from 0.33.0 to 0.34.0.
- [Commits](https://github.com/golang/net/compare/v0.33.0...v0.34.0)

---
updated-dependencies:
- dependency-name: golang.org/x/net
  dependency-type: direct:production
  update-type: version-update:semver-minor
...

Signed-off-by: dependabot[bot] <support@github.com>
2025-01-07 00:38:29 +00:00
779d5e9b18
Merge pull request #65 from aykhans/refactor/rename-cli-flags
🔨 Rename CLI flags
2025-01-03 13:27:24 +04:00
6a79d0b1d7 🔖 bump version to 0.5.502 2025-01-03 13:26:34 +04:00
1e53b8a7fb 📚 Update docs 2025-01-03 13:26:00 +04:00
0a8dbec739
🔨 Renama JSON parameters 2025-01-03 13:17:20 +04:00
3762890914
🔨 Rename CLI flags 2025-01-03 13:14:25 +04:00
ca6b3d4eb2
Merge pull request #64 from aykhans/bump/version
🔖 Bump version to 0.5.501
2024-12-25 02:08:59 +04:00
1ee06aacc3
🔖 Bump version to 0.5.501 2024-12-25 02:08:47 +04:00
3d5834a6a6
Merge pull request #63 from aykhans/fix/empty-slice
🐛 Return empty brackets instead of null when slice length is 0
2024-12-25 01:24:28 +04:00
f1521cbb74
💄 Update config table 2024-12-23 18:58:22 +04:00
40f8a1cc37 🐛 Return empty brackets instead of null when slice length is 0 2024-12-22 23:15:10 +04:00
8a3574cd48
Merge pull request #62 from aykhans/bumg/version
🔖 Bump version to v0.5.5
2024-12-22 22:03:09 +04:00
5a2a4c47b2
🔖 Bump version to v0.5.5 2024-12-22 22:02:57 +04:00
ff09a3365e
Merge pull request #61 from aykhans/dependabot/go_modules/golang.org/x/net-0.33.0
Bump golang.org/x/net from 0.32.0 to 0.33.0
2024-12-20 18:17:30 +04:00
c83246abe4
Merge pull request #60 from aykhans/refactor/json-marshal
Refactor json marshal
2024-12-20 18:17:11 +04:00
efc4c62001 📚 Update docs 2024-12-20 18:16:25 +04:00
dependabot[bot]
f567774e3a
Bump golang.org/x/net from 0.32.0 to 0.33.0
Bumps [golang.org/x/net](https://github.com/golang/net) from 0.32.0 to 0.33.0.
- [Commits](https://github.com/golang/net/compare/v0.32.0...v0.33.0)

---
updated-dependencies:
- dependency-name: golang.org/x/net
  dependency-type: direct:production
  update-type: version-update:semver-minor
...

Signed-off-by: dependabot[bot] <support@github.com>
2024-12-20 01:01:35 +00:00
d2bd60e3ff 🔨 Add custom 'WidthMaxEnforcer' to the table config 2024-12-20 01:21:26 +04:00
0fe782c768 🔨 Add 'PrettyJSONMarshal' function 2024-12-20 01:20:45 +04:00
b9dd14a588
Merge pull request #59 from aykhans/refactor/response-print
🔨 Round off durations before printing
2024-12-19 23:22:06 +04:00
18b1c7dae3 🔨 Round off durations before printing 2024-12-19 21:40:53 +04:00
77ddcf722a
Merge pull request #58 from aykhans/hotfix/maxuint
🚑 Replace 'math.MaxUint32' with static variable
2024-12-18 23:47:53 +04:00
048cb68898 🚑 Replace 'math.MaxUint32' with static variable 2024-12-18 23:47:24 +04:00
fd5e87ee68
Merge pull request #57 from aykhans/refactor/cli-readers
🔨 Cobra package replaced with standard flag package
2024-12-18 23:40:03 +04:00
61b9ce943b 📚 Update docs 2024-12-18 23:38:52 +04:00
6fceb0094b 🔖 Bump version to 0.5.4 2024-12-18 23:31:13 +04:00
186683fce5 🔨Cobra package replaced with standard flag package 2024-12-18 20:58:14 +04:00
04e5b5f3eb
Merge pull request #56 from aykhans/bump/version
Bump version to 0.5.3
2024-12-18 00:15:42 +04:00
2bebf09adb
Bump version to 0.5.3 2024-12-18 00:15:31 +04:00
ebacebff16
Merge pull request #55 from aykhans/refactor/response-struct
🔨 Reduce memory usage of the 'Response' struct
2024-12-17 23:22:30 +04:00
575ca2913a 🔨 Reduce memory usage of the 'Response' struct 2024-12-17 23:07:25 +04:00
4686e26ede
Merge pull request #53 from aykhans/feat/add-metrics
Add latency percentiles to the results table
2024-12-17 17:51:29 +04:00
bd33b2c6a2 Add latency percentiles to the results table 2024-12-17 03:42:52 +04:00
3068e0acae 🔨 Add 'types' package 2024-12-17 02:46:55 +04:00
7a40bbbd3c
Merge pull request #51 from aykhans/dependabot/go_modules/go_modules-5a9c29dde4
Bump golang.org/x/crypto from 0.30.0 to 0.31.0 in the go_modules group
2024-12-16 22:59:56 +04:00
dependabot[bot]
43c0bd2138
Bump golang.org/x/crypto from 0.30.0 to 0.31.0 in the go_modules group
Bumps the go_modules group with 1 update: [golang.org/x/crypto](https://github.com/golang/crypto).


Updates `golang.org/x/crypto` from 0.30.0 to 0.31.0
- [Commits](https://github.com/golang/crypto/compare/v0.30.0...v0.31.0)

---
updated-dependencies:
- dependency-name: golang.org/x/crypto
  dependency-type: indirect
  dependency-group: go_modules
...

Signed-off-by: dependabot[bot] <support@github.com>
2024-12-16 18:57:55 +00:00
fddeec1791
Merge pull request #50 from aykhans/dependabot/go_modules/github.com/jedib0t/go-pretty/v6-6.6.5
Bump github.com/jedib0t/go-pretty/v6 from 6.6.4 to 6.6.5
2024-12-16 22:56:58 +04:00
dependabot[bot]
6f5fa1860c
Bump github.com/jedib0t/go-pretty/v6 from 6.6.4 to 6.6.5
Bumps [github.com/jedib0t/go-pretty/v6](https://github.com/jedib0t/go-pretty) from 6.6.4 to 6.6.5.
- [Release notes](https://github.com/jedib0t/go-pretty/releases)
- [Commits](https://github.com/jedib0t/go-pretty/compare/v6.6.4...v6.6.5)

---
updated-dependencies:
- dependency-name: github.com/jedib0t/go-pretty/v6
  dependency-type: direct:production
  update-type: version-update:semver-patch
...

Signed-off-by: dependabot[bot] <support@github.com>
2024-12-16 00:53:59 +00:00
4480aed0f0
Merge pull request #48 from aykhans/dependabot/go_modules/github.com/valyala/fasthttp-1.58.0
Bump github.com/valyala/fasthttp from 1.57.0 to 1.58.0
2024-12-10 15:18:36 +04:00
dependabot[bot]
f5144ac4be
Bump github.com/valyala/fasthttp from 1.57.0 to 1.58.0
Bumps [github.com/valyala/fasthttp](https://github.com/valyala/fasthttp) from 1.57.0 to 1.58.0.
- [Release notes](https://github.com/valyala/fasthttp/releases)
- [Commits](https://github.com/valyala/fasthttp/compare/v1.57.0...v1.58.0)

---
updated-dependencies:
- dependency-name: github.com/valyala/fasthttp
  dependency-type: direct:production
  update-type: version-update:semver-minor
...

Signed-off-by: dependabot[bot] <support@github.com>
2024-12-10 10:53:23 +00:00
4a1051e08c
Merge pull request #47 from aykhans/dependabot/go_modules/github.com/jedib0t/go-pretty/v6-6.6.4
Bump github.com/jedib0t/go-pretty/v6 from 6.6.3 to 6.6.4
2024-12-10 14:52:10 +04:00
dependabot[bot]
2371986909
Bump github.com/jedib0t/go-pretty/v6 from 6.6.3 to 6.6.4
Bumps [github.com/jedib0t/go-pretty/v6](https://github.com/jedib0t/go-pretty) from 6.6.3 to 6.6.4.
- [Release notes](https://github.com/jedib0t/go-pretty/releases)
- [Commits](https://github.com/jedib0t/go-pretty/compare/v6.6.3...v6.6.4)

---
updated-dependencies:
- dependency-name: github.com/jedib0t/go-pretty/v6
  dependency-type: direct:production
  update-type: version-update:semver-patch
...

Signed-off-by: dependabot[bot] <support@github.com>
2024-12-10 00:26:21 +00:00
e9af46084d
Merge pull request #45 from aykhans/docs/refactor
📚 Update README.md
2024-12-09 02:15:53 +04:00
cdaa63152f 📚 Update docs 2024-12-09 02:14:08 +04:00
16791421c7
📚 Update README.md 2024-12-09 02:11:52 +04:00
c90cfd8090
Merge pull request #44 from aykhans/dependabot/go_modules/golang.org/x/net-0.32.0
Bump golang.org/x/net from 0.31.0 to 0.32.0
2024-12-06 23:33:40 +04:00
dependabot[bot]
1aaea8e437
Bump golang.org/x/net from 0.31.0 to 0.32.0
Bumps [golang.org/x/net](https://github.com/golang/net) from 0.31.0 to 0.32.0.
- [Commits](https://github.com/golang/net/compare/v0.31.0...v0.32.0)

---
updated-dependencies:
- dependency-name: golang.org/x/net
  dependency-type: direct:production
  update-type: version-update:semver-minor
...

Signed-off-by: dependabot[bot] <support@github.com>
2024-12-06 00:40:06 +00:00
c44a0c978a
Merge pull request #43 from aykhans/dependabot/go_modules/github.com/jedib0t/go-pretty/v6-6.6.3
Bump github.com/jedib0t/go-pretty/v6 from 6.6.2 to 6.6.3
2024-12-01 20:54:09 +04:00
dependabot[bot]
1ace7be2b4
Bump github.com/jedib0t/go-pretty/v6 from 6.6.2 to 6.6.3
Bumps [github.com/jedib0t/go-pretty/v6](https://github.com/jedib0t/go-pretty) from 6.6.2 to 6.6.3.
- [Release notes](https://github.com/jedib0t/go-pretty/releases)
- [Commits](https://github.com/jedib0t/go-pretty/compare/v6.6.2...v6.6.3)

---
updated-dependencies:
- dependency-name: github.com/jedib0t/go-pretty/v6
  dependency-type: direct:production
  update-type: version-update:semver-patch
...

Signed-off-by: dependabot[bot] <support@github.com>
2024-11-29 00:50:11 +00:00
44 changed files with 2214 additions and 1543 deletions

View File

@ -4,6 +4,7 @@ binaries
dodo dodo
.git .git
.gitignore .gitignore
.golangci.yml
README.md README.md
LICENSE LICENSE
config.json config.json

25
.github/workflows/golangci-lint.yml vendored Normal file
View File

@ -0,0 +1,25 @@
name: golangci-lint
on:
push:
branches:
- main
pull_request:
permissions:
contents: read
jobs:
golangci:
name: lint
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- uses: actions/setup-go@v5
with:
go-version: stable
- name: golangci-lint
uses: golangci/golangci-lint-action@v7
with:
version: v2.0.2
args: --timeout=10m --config=.golangci.yml

33
.golangci.yml Normal file
View File

@ -0,0 +1,33 @@
version: "2"
run:
go: "1.24"
concurrency: 8
timeout: 10m
linters:
default: none
enable:
- asasalint
- asciicheck
- errcheck
- gomodguard
- goprintffuncname
- govet
- ineffassign
- misspell
- nakedret
- nolintlint
- prealloc
- reassign
- staticcheck
- unconvert
- unused
- whitespace
settings:
staticcheck:
checks:
- "all"
- "-S1002"
- "-ST1000"

View File

@ -1,19 +1,17 @@
FROM golang:1.23.2-alpine AS builder FROM golang:1.24-alpine AS builder
WORKDIR /dodo WORKDIR /src
COPY go.mod go.sum ./ COPY go.mod go.sum ./
RUN go mod download RUN go mod download
COPY . . COPY . .
RUN go build -ldflags "-s -w" -o dodo RUN go build -ldflags "-s -w" -o dodo
RUN echo "{}" > config.json
FROM gcr.io/distroless/static-debian12:latest FROM gcr.io/distroless/static-debian12:latest
WORKDIR /dodo WORKDIR /
COPY --from=builder /dodo/dodo /dodo/dodo COPY --from=builder /src/dodo /dodo
COPY --from=builder /dodo/config.json /dodo/config.json
ENTRYPOINT ["./dodo", "-c", "/dodo/config.json"] ENTRYPOINT ["./dodo"]

296
README.md
View File

@ -1,119 +1,263 @@
<h1 align="center">Dodo is a simple and easy-to-use HTTP benchmarking tool.</h1> <h1 align="center">Dodo - A Fast and Easy-to-Use HTTP Benchmarking Tool</h1>
<p align="center"> <p align="center">
<img width="30%" height="30%" src="https://ftp.aykhans.me/web/client/pubshares/hB6VSdCnBCr8gFPeiMuCji/browse?path=%2Fdodo.png"> <img width="30%" height="30%" src="https://ftp.aykhans.me/web/client/pubshares/VzPtSHS7yPQT7ngoZzZSNU/browse?path=%2Fdodo.png">
</p> </p>
## Table of Contents
- [Installation](#installation)
- [Using Docker (Recommended)](#using-docker-recommended)
- [Using Pre-built Binaries](#using-pre-built-binaries)
- [Building from Source](#building-from-source)
- [Usage](#usage)
- [1. CLI Usage](#1-cli-usage)
- [2. Config File Usage](#2-config-file-usage)
- [2.1 JSON Example](#21-json-example)
- [2.2 YAML/YML Example](#22-yamlyml-example)
- [3. CLI & Config File Combination](#3-cli--config-file-combination)
- [Config Parameters Reference](#config-parameters-reference)
## Installation ## Installation
### With Docker (Recommended)
Pull the Dodo image from Docker Hub: ### Using Docker (Recommended)
Pull the latest Dodo image from Docker Hub:
```sh ```sh
docker pull aykhans/dodo:latest docker pull aykhans/dodo:latest
``` ```
If you use Dodo with Docker and a config file, you must provide the config.json file as a volume to the Docker run command (not as the "-c config.json" argument), as shown in the examples in the [usage](#usage) section.
### With Binary File To use Dodo with Docker and a local config file, mount the config file as a volume and pass it as an argument:
You can grab binaries in the [releases](https://github.com/aykhans/dodo/releases) section.
### Build from Source ```sh
To build Dodo from source, you need to have [Go1.22+](https://golang.org/dl/) installed. <br> docker run -v /path/to/config.json:/config.json aykhans/dodo -f /config.json
Follow the steps below to build dodo: ```
1. **Clone the repository:** If you're using a remote config file via URL, you don't need to mount a volume:
```sh ```sh
git clone https://github.com/aykhans/dodo.git docker run aykhans/dodo -f https://raw.githubusercontent.com/aykhans/dodo/main/config.yaml
``` ```
2. **Navigate to the project directory:** ### Using Pre-built Binaries
```sh Download the latest binaries from the [releases](https://github.com/aykhans/dodo/releases) section.
cd dodo
```
3. **Build the project:** ### Building from Source
```sh To build Dodo from source, ensure you have [Go 1.24+](https://golang.org/dl/) installed.
go build -ldflags "-s -w" -o dodo
```
This will generate an executable named `dodo` in the project directory. ```sh
go install -ldflags "-s -w" github.com/aykhans/dodo@latest
```
## Usage ## Usage
You can use Dodo with CLI arguments, a JSON config file, or both. If you use both, CLI arguments will always override JSON config arguments if there is a conflict.
### 1. CLI Dodo supports CLI arguments, configuration files (JSON/YAML), or a combination of both. If both are used, CLI arguments take precedence.
Send 1000 GET requests to https://example.com with 10 parallel dodos (threads) and a timeout of 2000 milliseconds:
### 1. CLI Usage
Send 1000 GET requests to https://example.com with 10 parallel dodos (threads), each with a timeout of 2 seconds, within a maximum duration of 1 minute:
```sh ```sh
dodo -u https://example.com -m GET -d 10 -r 1000 -t 2000 dodo -u https://example.com -m GET -d 10 -r 1000 -o 1m -t 2s
``` ```
With Docker: With Docker:
```sh ```sh
docker run --rm aykhans/dodo -u https://example.com -m GET -d 10 -r 1000 -t 2000 docker run --rm -i aykhans/dodo -u https://example.com -m GET -d 10 -r 1000 -o 1m -t 2s
``` ```
### 2. JSON config file ### 2. Config File Usage
You can find an example config structure in the [config.json](https://github.com/aykhans/dodo/blob/main/config.json) file:
```json Send 1000 GET requests to https://example.com with 10 parallel dodos (threads), each with a timeout of 800 milliseconds, within a maximum duration of 250 seconds:
#### 2.1 JSON Example
```jsonc
{ {
"method": "GET", "method": "GET",
"url": "https://example.com", "url": "https://example.com",
"no_proxy_check": false, "yes": false,
"timeout": 2000, "timeout": "800ms",
"dodos_count": 10, "dodos": 10,
"request_count": 1000, "requests": 1000,
"params": {}, "duration": "250s",
"headers": {},
"cookies": {}, "params": [
"body": [""], // A random value will be selected from the list for first "key1" param on each request
"proxies": [ // And always "value" for second "key1" param on each request
{ // e.g. "?key1=value2&key1=value"
"url": "http://example.com:8080", { "key1": ["value1", "value2", "value3", "value4"] },
"username": "username", { "key1": "value" },
"password": "password"
}, // A random value will be selected from the list for param "key2" on each request
{ // e.g. "?key2=value2"
"url": "http://example.com:8080" { "key2": ["value1", "value2"] },
} ],
]
"headers": [
// A random value will be selected from the list for first "key1" header on each request
// And always "value" for second "key1" header on each request
// e.g. "key1: value3", "key1: value"
{ "key1": ["value1", "value2", "value3", "value4"] },
{ "key1": "value" },
// A random value will be selected from the list for header "key2" on each request
// e.g. "key2: value2"
{ "key2": ["value1", "value2"] },
],
"cookies": [
// A random value will be selected from the list for first "key1" cookie on each request
// And always "value" for second "key1" cookie on each request
// e.g. "key1=value4; key1=value"
{ "key1": ["value1", "value2", "value3", "value4"] },
{ "key1": "value" },
// A random value will be selected from the list for cookie "key2" on each request
// e.g. "key2=value1"
{ "key2": ["value1", "value2"] },
],
"body": "body-text",
// OR
// A random body value will be selected from the list for each request
"body": ["body-text1", "body-text2", "body-text3"],
"proxy": "http://example.com:8080",
// OR
// A random proxy will be selected from the list for each request
"proxy": [
"http://example.com:8080",
"http://username:password@example.com:8080",
"socks5://example.com:8080",
"socks5h://example.com:8080",
],
} }
``` ```
Send 1000 GET requests to https://example.com with 10 parallel dodos (threads) and a timeout of 2000 milliseconds:
```sh ```sh
dodo -c /path/config.json dodo -f /path/config.json
# OR
dodo -f https://example.com/config.json
``` ```
With Docker: With Docker:
```sh
docker run --rm -v ./path/config.json:/dodo/config.json -i aykhans/dodo
```
### 3. Both (CLI & JSON)
Override the config file arguments with CLI arguments:
```sh ```sh
dodo -c /path/config.json -u https://example.com -m GET -d 10 -r 1000 -t 2000 docker run --rm -i -v /path/to/config.json:/config.json aykhans/dodo
# OR
docker run --rm -i aykhans/dodo -f https://example.com/config.json
``` ```
#### 2.2 YAML/YML Example
```yaml
method: "GET"
url: "https://example.com"
yes: false
timeout: "800ms"
dodos: 10
requests: 1000
duration: "250s"
params:
# A random value will be selected from the list for first "key1" param on each request
# And always "value" for second "key1" param on each request
# e.g. "?key1=value2&key1=value"
- key1: ["value1", "value2", "value3", "value4"]
- key1: "value"
# A random value will be selected from the list for param "key2" on each request
# e.g. "?key2=value2"
- key2: ["value1", "value2"]
headers:
# A random value will be selected from the list for first "key1" header on each request
# And always "value" for second "key1" header on each request
# e.g. "key1: value3", "key1: value"
- key1: ["value1", "value2", "value3", "value4"]
- key1: "value"
# A random value will be selected from the list for header "key2" on each request
# e.g. "key2: value2"
- key2: ["value1", "value2"]
cookies:
# A random value will be selected from the list for first "key1" cookie on each request
# And always "value" for second "key1" cookie on each request
# e.g. "key1=value4; key1=value"
- key1: ["value1", "value2", "value3", "value4"]
- key1: "value"
# A random value will be selected from the list for cookie "key2" on each request
# e.g. "key2=value1"
- key2: ["value1", "value2"]
body: "body-text"
# OR
# A random body value will be selected from the list for each request
body:
- "body-text1"
- "body-text2"
- "body-text3"
proxy: "http://example.com:8080"
# OR
# A random proxy will be selected from the list for each request
proxy:
- "http://example.com:8080"
- "http://username:password@example.com:8080"
- "socks5://example.com:8080"
- "socks5h://example.com:8080"
```
```sh
dodo -f /path/config.yaml
# OR
dodo -f https://example.com/config.yaml
```
With Docker: With Docker:
```sh ```sh
docker run --rm -v ./path/config.json:/dodo/config.json -i aykhans/dodo -u https://example.com -m GET -d 10 -r 1000 -t 2000 docker run --rm -i -v /path/to/config.yaml:/config.yaml aykhans/dodo -f /config.yaml
# OR
docker run --rm -i aykhans/dodo -f https://example.com/config.yaml
``` ```
## CLI and JSON Config Parameters ### 3. CLI & Config File Combination
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 | CLI arguments override config file values:
| --------------------- | ---------------- | --------------- | -------------- | -------------------------------- | ------------------------------------------------------------------- | ----------- |
| Config file | - | --config-file | -c | String | Path to the JSON config file | - | ```sh
| Yes | - | --yes | -y | Boolean | Answer yes to all questions | false | dodo -f /path/to/config.yaml -u https://example.com -m GET -d 10 -r 1000 -o 1m -t 5s
| URL | url | --url | -u | String | URL to send the request to | - | ```
| Method | method | --method | -m | String | HTTP method | GET |
| Request count | request_count | --request-count | -r | Integer | Total number of requests to send | 1000 | With Docker:
| Dodos count (Threads) | dodos_count | --dodos-count | -d | Integer | Number of dodos (threads) to send requests in parallel | 1 |
| Timeout | timeout | --timeout | -t | Integer | Timeout for canceling each request (milliseconds) | 10000 | ```sh
| No Proxy Check | no_proxy_check | --no-proxy-check| - | Boolean | Disable proxy check | false | docker run --rm -i -v /path/to/config.json:/config.json aykhans/dodo -f /config.json -u https://example.com -m GET -d 10 -r 1000 -o 1m -t 5s
| Params | params | - | - | Key-Value {String: [String]} | Request parameters | - | ```
| Headers | headers | - | - | Key-Value {String: [String]} | Request headers | - |
| Cookies | cookies | - | - | Key-Value {String: [String]} | Request cookies | - | ## Config Parameters Reference
| Body | body | - | - | [String] | Request body | - |
| Proxy | proxies | - | - | List[Key-Value {string: string}] | List of proxies (will check active proxies before sending requests) | - | If `Headers`, `Params`, `Cookies`, `Body`, or `Proxy` fields have multiple values, each request will choose a random value from the list.
| Parameter | config file | CLI Flag | CLI Short Flag | Type | Description | Default |
| --------------- | ----------- | ------------ | -------------- | ------------------------------ | ----------------------------------------------------------- | ------- |
| Config file | - | -config-file | -f | String | Path to local config file or http(s) URL of the config file | - |
| Yes | yes | -yes | -y | Boolean | Answer yes to all questions | false |
| URL | url | -url | -u | String | URL to send the request to | - |
| Method | method | -method | -m | String | HTTP method | GET |
| Dodos (Threads) | dodos | -dodos | -d | UnsignedInteger | Number of dodos (threads) to send requests in parallel | 1 |
| Requests | requests | -requests | -r | UnsignedInteger | Total number of requests to send | - |
| Duration | duration | -duration | -o | Time | Maximum duration for the test | - |
| Timeout | timeout | -timeout | -t | Time | Timeout for canceling each request | 10s |
| Params | params | -param | -p | [{String: String OR [String]}] | Request parameters | - |
| Headers | headers | -header | -H | [{String: String OR [String]}] | Request headers | - |
| Cookies | cookies | -cookie | -c | [{String: String OR [String]}] | Request cookies | - |
| Body | body | -body | -b | String OR [String] | Request body or list of request bodies | - |
| Proxy | proxies | -proxy | -x | String OR [String] | Proxy URL or list of proxy URLs | - |

45
Taskfile.yaml Normal file
View File

@ -0,0 +1,45 @@
# https://taskfile.dev
version: "3"
vars:
PLATFORMS:
- os: darwin
archs: [amd64, arm64]
- os: freebsd
archs: [386, amd64, arm]
- os: linux
archs: [386, amd64, arm, arm64]
- os: netbsd
archs: [386, amd64, arm]
- os: openbsd
archs: [386, amd64, arm, arm64]
- os: windows
archs: [386, amd64, arm64]
tasks:
run: go run main.go
fmt: gofmt -w -d .
lint: golangci-lint run
build: go build -ldflags "-s -w" -o "dodo"
build-all:
silent: true
cmds:
- rm -rf binaries
- |
{{ $ext := "" }}
{{- range $platform := .PLATFORMS }}
{{- if eq $platform.os "windows" }}
{{ $ext = ".exe" }}
{{- end }}
{{- range $arch := $platform.archs }}
echo "Building for {{$platform.os}}/{{$arch}}"
GOOS={{$platform.os}} GOARCH={{$arch}} go build -ldflags "-s -w" -o "./binaries/dodo-{{$platform.os}}-{{$arch}}{{$ext}}"
{{- end }}
{{- end }}
- echo -e "\033[32m*** Build completed ***\033[0m"

View File

@ -1,32 +0,0 @@
#!/bin/bash
platforms=(
"darwin,amd64"
"darwin,arm64"
"freebsd,386"
"freebsd,amd64"
"freebsd,arm"
"linux,386"
"linux,amd64"
"linux,arm"
"linux,arm64"
"netbsd,386"
"netbsd,amd64"
"netbsd,arm"
"openbsd,386"
"openbsd,amd64"
"openbsd,arm"
"openbsd,arm64"
"windows,386"
"windows,amd64"
"windows,arm64"
)
for platform in "${platforms[@]}"; do
IFS=',' read -r build_os build_arch <<< "$platform"
ext=""
if [ "$build_os" == "windows" ]; then
ext=".exe"
fi
GOOS="$build_os" GOARCH="$build_arch" go build -ldflags "-s -w" -o "./binaries/dodo-$build_os-$build_arch$ext"
done

View File

@ -1,22 +1,36 @@
{ {
"method": "GET", "method": "GET",
"url": "https://example.com", "url": "https://example.com",
"no_proxy_check": true, "yes": false,
"timeout": 10000, "timeout": "5s",
"dodos_count": 50, "dodos": 8,
"request_count": 1000, "requests": 1000,
"params": {}, "duration": "10s",
"headers": {},
"cookies": {}, "params": [
"body": [""], { "key1": ["value1", "value2", "value3", "value4"] },
"proxies": [ { "key1": "value" },
{ { "key2": ["value1", "value2"] }
"url": "http://example.com:8080", ],
"username": "username",
"password": "password" "headers": [
}, { "key1": ["value1", "value2", "value3", "value4"] },
{ { "key1": "value" },
"url": "http://example.com:8080" { "key2": ["value1", "value2"] }
} ],
"cookies": [
{ "key1": ["value1", "value2", "value3", "value4"] },
{ "key1": "value" },
{ "key2": ["value1", "value2"] }
],
"body": ["body-text1", "body-text2", "body-text3"],
"proxy": [
"http://example.com:8080",
"http://username:password@example.com:8080",
"socks5://example.com:8080",
"socks5h://example.com:8080"
] ]
} }

39
config.yaml Normal file
View File

@ -0,0 +1,39 @@
method: "GET"
url: "https://example.com"
yes: false
timeout: "5s"
dodos: 8
requests: 1000
duration: "10s"
params:
- key1: ["value1", "value2", "value3", "value4"]
- key1: "value"
- key2: ["value1", "value2"]
headers:
- key1: ["value1", "value2", "value3", "value4"]
- key1: "value"
- key2: ["value1", "value2"]
cookies:
- key1: ["value1", "value2", "value3", "value4"]
- key1: "value"
- key2: ["value1", "value2"]
# body: "body-text"
# OR
# A random body value will be selected from the list for each request
body:
- "body-text1"
- "body-text2"
- "body-text3"
# proxy: "http://example.com:8080"
# OR
# A random proxy will be selected from the list for each request
proxy:
- "http://example.com:8080"
- "http://username:password@example.com:8080"
- "socks5://example.com:8080"
- "socks5h://example.com:8080"

181
config/cli.go Normal file
View File

@ -0,0 +1,181 @@
package config
import (
"flag"
"fmt"
"os"
"strings"
"time"
"github.com/aykhans/dodo/types"
"github.com/aykhans/dodo/utils"
)
const cliUsageText = `Usage:
dodo [flags]
Examples:
Simple usage:
dodo -u https://example.com -o 1m
Usage with config file:
dodo -f /path/to/config/file/config.json
Usage with all flags:
dodo -f /path/to/config/file/config.json \
-u https://example.com -m POST \
-d 10 -r 1000 -o 3m -t 3s \
-b "body1" -body "body2" \
-H "header1:value1" -header "header2:value2" \
-p "param1=value1" -param "param2=value2" \
-c "cookie1=value1" -cookie "cookie2=value2" \
-x "http://proxy.example.com:8080" -proxy "socks5://proxy2.example.com:8080" \
-y
Flags:
-h, -help help for dodo
-v, -version version for dodo
-y, -yes bool Answer yes to all questions (default %v)
-f, -config-file string Path to the local config file or http(s) URL of the config file
-d, -dodos uint Number of dodos(threads) (default %d)
-r, -requests uint Number of total requests
-o, -duration Time Maximum duration for the test (e.g. 30s, 1m, 5h)
-t, -timeout Time Timeout for each request (e.g. 400ms, 15s, 1m10s) (default %v)
-u, -url string URL for stress testing
-m, -method string HTTP Method for the request (default %s)
-b, -body [string] Body for the request (e.g. "body text")
-p, -param [string] Parameter for the request (e.g. "key1=value1")
-H, -header [string] Header for the request (e.g. "key1:value1")
-c, -cookie [string] Cookie for the request (e.g. "key1=value1")
-x, -proxy [string] Proxy for the request (e.g. "http://proxy.example.com:8080")`
func (config *Config) ReadCLI() (types.ConfigFile, error) {
flag.Usage = func() {
fmt.Printf(
cliUsageText+"\n",
DefaultYes,
DefaultDodosCount,
DefaultTimeout,
DefaultMethod,
)
}
var (
version = false
configFile = ""
yes = false
method = ""
url types.RequestURL
dodosCount = uint(0)
requestCount = uint(0)
timeout time.Duration
duration time.Duration
)
{
flag.BoolVar(&version, "version", false, "Prints the version of the program")
flag.BoolVar(&version, "v", false, "Prints the version of the program")
flag.StringVar(&configFile, "config-file", "", "Path to the configuration file")
flag.StringVar(&configFile, "f", "", "Path to the configuration file")
flag.BoolVar(&yes, "yes", false, "Answer yes to all questions")
flag.BoolVar(&yes, "y", false, "Answer yes to all questions")
flag.StringVar(&method, "method", "", "HTTP Method")
flag.StringVar(&method, "m", "", "HTTP Method")
flag.Var(&url, "url", "URL to send the request")
flag.Var(&url, "u", "URL to send the request")
flag.UintVar(&dodosCount, "dodos", 0, "Number of dodos(threads)")
flag.UintVar(&dodosCount, "d", 0, "Number of dodos(threads)")
flag.UintVar(&requestCount, "requests", 0, "Number of total requests")
flag.UintVar(&requestCount, "r", 0, "Number of total requests")
flag.DurationVar(&duration, "duration", 0, "Maximum duration of the test")
flag.DurationVar(&duration, "o", 0, "Maximum duration of the test")
flag.DurationVar(&timeout, "timeout", 0, "Timeout for each request (e.g. 400ms, 15s, 1m10s)")
flag.DurationVar(&timeout, "t", 0, "Timeout for each request (e.g. 400ms, 15s, 1m10s)")
flag.Var(&config.Params, "param", "URL parameter to send with the request")
flag.Var(&config.Params, "p", "URL parameter to send with the request")
flag.Var(&config.Headers, "header", "Header to send with the request")
flag.Var(&config.Headers, "H", "Header to send with the request")
flag.Var(&config.Cookies, "cookie", "Cookie to send with the request")
flag.Var(&config.Cookies, "c", "Cookie to send with the request")
flag.Var(&config.Body, "body", "Body to send with the request")
flag.Var(&config.Body, "b", "Body to send with the request")
flag.Var(&config.Proxies, "proxy", "Proxy to use for the request")
flag.Var(&config.Proxies, "x", "Proxy to use for the request")
}
flag.Parse()
if len(os.Args) <= 1 {
flag.CommandLine.Usage()
os.Exit(0)
}
if args := flag.Args(); len(args) > 0 {
return types.ConfigFile(configFile), fmt.Errorf("unexpected arguments: %v", strings.Join(args, ", "))
}
if version {
fmt.Printf("dodo version %s\n", VERSION)
os.Exit(0)
}
flag.Visit(func(f *flag.Flag) {
switch f.Name {
case "method", "m":
config.Method = utils.ToPtr(method)
case "url", "u":
config.URL = utils.ToPtr(url)
case "dodos", "d":
config.DodosCount = utils.ToPtr(dodosCount)
case "requests", "r":
config.RequestCount = utils.ToPtr(requestCount)
case "duration", "o":
config.Duration = &types.Duration{Duration: duration}
case "timeout", "t":
config.Timeout = &types.Timeout{Duration: timeout}
case "yes", "y":
config.Yes = utils.ToPtr(yes)
}
})
return types.ConfigFile(configFile), nil
}
// CLIYesOrNoReader reads a yes or no answer from the command line.
// It prompts the user with the given message and default value,
// and returns true if the user answers "y" or "Y", and false otherwise.
// If there is an error while reading the input, it returns false.
// If the user simply presses enter without providing any input,
// it returns the default value specified by the `dft` parameter.
func CLIYesOrNoReader(message string, dft bool) bool {
var answer string
defaultMessage := "Y/n"
if !dft {
defaultMessage = "y/N"
}
fmt.Printf("%s [%s]: ", message, defaultMessage)
if _, err := fmt.Scanln(&answer); err != nil {
if err.Error() == "unexpected newline" {
return dft
}
return false
}
if answer == "" {
return dft
}
return answer == "y" || answer == "Y"
}

View File

@ -1,194 +1,225 @@
package config package config
import ( import (
"errors"
"fmt" "fmt"
"net/url" "net/url"
"os" "os"
"slices"
"strings"
"time" "time"
"github.com/aykhans/dodo/types"
"github.com/aykhans/dodo/utils" "github.com/aykhans/dodo/utils"
"github.com/jedib0t/go-pretty/v6/table" "github.com/jedib0t/go-pretty/v6/table"
) )
const ( const (
VERSION string = "0.5.2" VERSION string = "0.6.3"
DefaultUserAgent string = "Dodo/" + VERSION DefaultUserAgent string = "Dodo/" + VERSION
ProxyCheckURL string = "https://www.google.com"
DefaultMethod string = "GET" DefaultMethod string = "GET"
DefaultTimeout uint32 = 10000 // Milliseconds (10 seconds) DefaultTimeout time.Duration = time.Second * 10
DefaultDodosCount uint = 1 DefaultDodosCount uint = 1
DefaultRequestCount uint = 1000 DefaultRequestCount uint = 0
MaxDodosCountForProxies uint = 20 // Max dodos count for proxy check DefaultDuration time.Duration = 0
DefaultYes bool = false
) )
var SupportedProxySchemes []string = []string{"http", "socks5", "socks5h"}
type RequestConfig struct { type RequestConfig struct {
Method string Method string
URL *url.URL URL url.URL
Timeout time.Duration Timeout time.Duration
DodosCount uint DodosCount uint
RequestCount uint RequestCount uint
Params map[string][]string Duration time.Duration
Headers map[string][]string
Cookies map[string][]string
Proxies []Proxy
Body []string
Yes bool Yes bool
NoProxyCheck bool Params types.Params
Headers types.Headers
Cookies types.Cookies
Body types.Body
Proxies types.Proxies
} }
func (config *RequestConfig) Print() { func NewRequestConfig(conf *Config) *RequestConfig {
t := table.NewWriter() return &RequestConfig{
t.SetOutputMirror(os.Stdout) Method: *conf.Method,
t.SetStyle(table.StyleLight) URL: conf.URL.URL,
t.SetColumnConfigs([]table.ColumnConfig{ Timeout: conf.Timeout.Duration,
{Number: 2, WidthMax: 50}, DodosCount: *conf.DodosCount,
}) RequestCount: *conf.RequestCount,
Duration: conf.Duration.Duration,
newHeaders := make(map[string][]string) Yes: *conf.Yes,
newHeaders["User-Agent"] = []string{DefaultUserAgent} Params: conf.Params,
for k, v := range config.Headers { Headers: conf.Headers,
newHeaders[k] = v Cookies: conf.Cookies,
Body: conf.Body,
Proxies: conf.Proxies,
} }
t.AppendHeader(table.Row{"Request Configuration"})
t.AppendRow(table.Row{"Method", config.Method})
t.AppendSeparator()
t.AppendRow(table.Row{"URL", config.URL})
t.AppendSeparator()
t.AppendRow(table.Row{"Timeout", fmt.Sprintf("%dms", config.Timeout/time.Millisecond)})
t.AppendSeparator()
t.AppendRow(table.Row{"Dodos", config.DodosCount})
t.AppendSeparator()
t.AppendRow(table.Row{"Requests", config.RequestCount})
t.AppendSeparator()
t.AppendRow(table.Row{"Params", utils.MarshalJSON(config.Params, 3)})
t.AppendSeparator()
t.AppendRow(table.Row{"Headers", utils.MarshalJSON(newHeaders, 3)})
t.AppendSeparator()
t.AppendRow(table.Row{"Cookies", utils.MarshalJSON(config.Cookies, 3)})
t.AppendSeparator()
t.AppendRow(table.Row{"Proxies Count", len(config.Proxies)})
t.AppendSeparator()
t.AppendRow(table.Row{"Proxy Check", !config.NoProxyCheck})
t.AppendSeparator()
t.AppendRow(table.Row{"Body", utils.MarshalJSON(config.Body, 3)})
t.Render()
} }
func (config *RequestConfig) GetValidDodosCountForRequests() uint { func (rc *RequestConfig) GetValidDodosCountForRequests() uint {
return min(config.DodosCount, config.RequestCount) if rc.RequestCount == 0 {
return rc.DodosCount
}
return min(rc.DodosCount, rc.RequestCount)
} }
func (config *RequestConfig) GetValidDodosCountForProxies() uint { func (rc *RequestConfig) GetMaxConns(minConns uint) uint {
return min(config.DodosCount, uint(len(config.Proxies)), MaxDodosCountForProxies)
}
func (config *RequestConfig) GetMaxConns(minConns uint) uint {
maxConns := max( maxConns := max(
minConns, uint(config.GetValidDodosCountForRequests()), minConns, rc.GetValidDodosCountForRequests(),
) )
return ((maxConns * 50 / 100) + maxConns) return ((maxConns * 50 / 100) + maxConns)
} }
func (rc *RequestConfig) Print() {
t := table.NewWriter()
t.SetOutputMirror(os.Stdout)
t.SetStyle(table.StyleLight)
t.SetColumnConfigs([]table.ColumnConfig{
{
Number: 2,
WidthMaxEnforcer: func(col string, maxLen int) string {
lines := strings.Split(col, "\n")
for i, line := range lines {
if len(line) > maxLen {
lines[i] = line[:maxLen-3] + "..."
}
}
return strings.Join(lines, "\n")
},
WidthMax: 50},
})
t.AppendHeader(table.Row{"Request Configuration"})
t.AppendRow(table.Row{"URL", rc.URL.String()})
t.AppendSeparator()
t.AppendRow(table.Row{"Method", rc.Method})
t.AppendSeparator()
t.AppendRow(table.Row{"Timeout", rc.Timeout})
t.AppendSeparator()
t.AppendRow(table.Row{"Dodos", rc.DodosCount})
t.AppendSeparator()
if rc.RequestCount > 0 {
t.AppendRow(table.Row{"Requests", rc.RequestCount})
} else {
t.AppendRow(table.Row{"Requests"})
}
t.AppendSeparator()
if rc.Duration > 0 {
t.AppendRow(table.Row{"Duration", rc.Duration})
} else {
t.AppendRow(table.Row{"Duration"})
}
t.AppendSeparator()
t.AppendRow(table.Row{"Params", rc.Params.String()})
t.AppendSeparator()
t.AppendRow(table.Row{"Headers", rc.Headers.String()})
t.AppendSeparator()
t.AppendRow(table.Row{"Cookies", rc.Cookies.String()})
t.AppendSeparator()
t.AppendRow(table.Row{"Proxy", rc.Proxies.String()})
t.AppendSeparator()
t.AppendRow(table.Row{"Body", rc.Body.String()})
t.Render()
}
type Config struct { type Config struct {
Method string `json:"method" validate:"http_method"` // custom validations: http_method Method *string `json:"method" yaml:"method"`
URL string `json:"url" validate:"http_url,required"` URL *types.RequestURL `json:"url" yaml:"url"`
Timeout uint32 `json:"timeout" validate:"gte=1,lte=100000"` Timeout *types.Timeout `json:"timeout" yaml:"timeout"`
DodosCount uint `json:"dodos_count" validate:"gte=1"` DodosCount *uint `json:"dodos" yaml:"dodos"`
RequestCount uint `json:"request_count" validation_name:"request-count" validate:"gte=1"` RequestCount *uint `json:"requests" yaml:"requests"`
NoProxyCheck utils.Option[bool] `json:"no_proxy_check"` Duration *types.Duration `json:"duration" yaml:"duration"`
Yes *bool `json:"yes" yaml:"yes"`
Params types.Params `json:"params" yaml:"params"`
Headers types.Headers `json:"headers" yaml:"headers"`
Cookies types.Cookies `json:"cookies" yaml:"cookies"`
Body types.Body `json:"body" yaml:"body"`
Proxies types.Proxies `json:"proxy" yaml:"proxy"`
} }
func NewConfig( func NewConfig() *Config {
method string, return &Config{}
timeout uint32,
dodosCount uint,
requestCount uint,
noProxyCheck utils.Option[bool],
) *Config {
if noProxyCheck == nil {
noProxyCheck = utils.NewNoneOption[bool]()
}
return &Config{
Method: method,
Timeout: timeout,
DodosCount: dodosCount,
RequestCount: requestCount,
NoProxyCheck: noProxyCheck,
}
} }
func (config *Config) MergeConfigs(newConfig *Config) { func (config *Config) Validate() []error {
if newConfig.Method != "" { var errs []error
if utils.IsNilOrZero(config.URL) {
errs = append(errs, errors.New("request URL is required"))
} else {
if config.URL.Scheme == "" {
config.URL.Scheme = "http"
}
if config.URL.Scheme != "http" && config.URL.Scheme != "https" {
errs = append(errs, errors.New("request URL scheme must be http or https"))
}
urlParams := types.Params{}
for key, values := range config.URL.Query() {
for _, value := range values {
urlParams = append(urlParams, types.KeyValue[string, []string]{
Key: key,
Value: []string{value},
})
}
}
config.Params = append(urlParams, config.Params...)
config.URL.RawQuery = ""
}
if utils.IsNilOrZero(config.Method) {
errs = append(errs, errors.New("request method is required"))
}
if utils.IsNilOrZero(config.Timeout) {
errs = append(errs, errors.New("request timeout must be greater than 0"))
}
if utils.IsNilOrZero(config.DodosCount) {
errs = append(errs, errors.New("dodos count must be greater than 0"))
}
if utils.IsNilOrZero(config.Duration) && utils.IsNilOrZero(config.RequestCount) {
errs = append(errs, errors.New("you should provide at least one of duration or request count"))
}
for i, proxy := range config.Proxies {
if proxy.String() == "" {
errs = append(errs, fmt.Errorf("proxies[%d]: proxy cannot be empty", i))
} else if schema := proxy.Scheme; !slices.Contains(SupportedProxySchemes, schema) {
errs = append(errs,
fmt.Errorf("proxies[%d]: proxy has unsupported scheme \"%s\" (supported schemes: %s)",
i, proxy.String(), strings.Join(SupportedProxySchemes, ", "),
),
)
}
}
return errs
}
func (config *Config) MergeConfig(newConfig *Config) {
if newConfig.Method != nil {
config.Method = newConfig.Method config.Method = newConfig.Method
} }
if newConfig.URL != "" { if newConfig.URL != nil {
config.URL = newConfig.URL config.URL = newConfig.URL
} }
if newConfig.Timeout != 0 { if newConfig.Timeout != nil {
config.Timeout = newConfig.Timeout config.Timeout = newConfig.Timeout
} }
if newConfig.DodosCount != 0 { if newConfig.DodosCount != nil {
config.DodosCount = newConfig.DodosCount config.DodosCount = newConfig.DodosCount
} }
if newConfig.RequestCount != 0 { if newConfig.RequestCount != nil {
config.RequestCount = newConfig.RequestCount config.RequestCount = newConfig.RequestCount
} }
if !newConfig.NoProxyCheck.IsNone() { if newConfig.Duration != nil {
config.NoProxyCheck = newConfig.NoProxyCheck config.Duration = newConfig.Duration
} }
} 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 = utils.NewOption(false)
}
}
type Proxy struct {
URL string `json:"url" validate:"required,proxy_url"`
Username string `json:"username"`
Password string `json:"password"`
}
type JSONConfig struct {
*Config
Params map[string][]string `json:"params"`
Headers map[string][]string `json:"headers"`
Cookies map[string][]string `json:"cookies"`
Proxies []Proxy `json:"proxies" validate:"dive"`
Body []string `json:"body"`
}
func NewJSONConfig(
config *Config,
params map[string][]string,
headers map[string][]string,
cookies map[string][]string,
proxies []Proxy,
body []string,
) *JSONConfig {
return &JSONConfig{
config, params, headers, cookies, proxies, body,
}
}
func (config *JSONConfig) MergeConfigs(newConfig *JSONConfig) {
config.Config.MergeConfigs(newConfig.Config)
if len(newConfig.Params) != 0 { if len(newConfig.Params) != 0 {
config.Params = newConfig.Params config.Params = newConfig.Params
} }
@ -206,25 +237,24 @@ func (config *JSONConfig) MergeConfigs(newConfig *JSONConfig) {
} }
} }
type CLIConfig struct { func (config *Config) SetDefaults() {
*Config if config.Method == nil {
Yes bool `json:"yes" validate:"omitempty"` config.Method = utils.ToPtr(DefaultMethod)
ConfigFile string `validation_name:"config-file" validate:"omitempty,filepath"`
}
func NewCLIConfig(
config *Config,
yes bool,
configFile string,
) *CLIConfig {
return &CLIConfig{
config, yes, configFile,
} }
} if config.Timeout == nil {
config.Timeout = &types.Timeout{Duration: DefaultTimeout}
func (config *CLIConfig) MergeConfigs(newConfig *CLIConfig) {
config.Config.MergeConfigs(newConfig.Config)
if newConfig.ConfigFile != "" {
config.ConfigFile = newConfig.ConfigFile
} }
if config.DodosCount == nil {
config.DodosCount = utils.ToPtr(DefaultDodosCount)
}
if config.RequestCount == nil {
config.RequestCount = utils.ToPtr(DefaultRequestCount)
}
if config.Duration == nil {
config.Duration = &types.Duration{Duration: DefaultDuration}
}
if config.Yes == nil {
config.Yes = utils.ToPtr(DefaultYes)
}
config.Headers.SetIfNotExists("User-Agent", DefaultUserAgent)
} }

84
config/file.go Normal file
View File

@ -0,0 +1,84 @@
package config
import (
"encoding/json"
"errors"
"fmt"
"io"
"net/http"
"os"
"slices"
"strings"
"time"
"github.com/aykhans/dodo/types"
"gopkg.in/yaml.v3"
)
var supportedFileTypes = []string{"json", "yaml", "yml"}
func (config *Config) ReadFile(filePath types.ConfigFile) error {
var (
data []byte
err error
)
fileExt := filePath.Extension()
if slices.Contains(supportedFileTypes, fileExt) {
if filePath.LocationType() == types.FileLocationTypeRemoteHTTP {
client := &http.Client{
Timeout: 10 * time.Second,
}
resp, err := client.Get(filePath.String())
if err != nil {
return fmt.Errorf("failed to fetch config file from %s", filePath)
}
defer func() { _ = resp.Body.Close() }()
data, err = io.ReadAll(io.Reader(resp.Body))
if err != nil {
return fmt.Errorf("failed to read config file from %s", filePath)
}
} else {
data, err = os.ReadFile(filePath.String())
if err != nil {
return errors.New("failed to read config file from " + filePath.String())
}
}
switch fileExt {
case "json":
return parseJSONConfig(data, config)
case "yml", "yaml":
return parseYAMLConfig(data, config)
}
}
return fmt.Errorf("unsupported config file type (supported types: %v)", strings.Join(supportedFileTypes, ", "))
}
func parseJSONConfig(data []byte, config *Config) error {
err := json.Unmarshal(data, &config)
if err != nil {
switch parsedErr := err.(type) {
case *json.SyntaxError:
return fmt.Errorf("JSON Config file: invalid syntax at byte offset %d", parsedErr.Offset)
case *json.UnmarshalTypeError:
return fmt.Errorf("JSON Config file: invalid type %v for field %s, expected %v", parsedErr.Value, parsedErr.Field, parsedErr.Type)
default:
return fmt.Errorf("JSON Config file: %s", err.Error())
}
}
return nil
}
func parseYAMLConfig(data []byte, config *Config) error {
err := yaml.Unmarshal(data, &config)
if err != nil {
return fmt.Errorf("YAML Config file: %s", err.Error())
}
return nil
}

View File

@ -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
}

View File

@ -1,72 +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 CobraErrorFormater(err error) error {
return err
}
func shortenNamespace(namespace string) string {
return namespace[strings.Index(namespace, ".")+1:]
}
func ValidationErrorsFormater(errs validator.ValidationErrors) error {
errsStr := make(map[string]string)
for _, err := range errs {
switch err.Tag() {
case "required":
errsStr[shortenNamespace(err.Namespace())] = fmt.Sprintf("Field \"%s\" is required", err.Field())
case "gte":
errsStr[shortenNamespace(err.Namespace())] = fmt.Sprintf("Value of \"%s\" must be greater than or equal to \"%s\"", err.Field(), err.Param())
case "lte":
errsStr[shortenNamespace(err.Namespace())] = fmt.Sprintf("Value of \"%s\" must be less than or equal to \"%s\"", err.Field(), err.Param())
case "filepath":
errsStr[shortenNamespace(err.Namespace())] = fmt.Sprintf("Invalid file path for \"%s\" field: \"%s\"", err.Field(), err.Value())
case "http_url":
errsStr[shortenNamespace(err.Namespace())] =
fmt.Sprintf("Invalid url for \"%s\" field: \"%s\"", err.Field(), err.Value())
// --------------------------------------| Custom validations |--------------------------------------
case "http_method":
errsStr[shortenNamespace(err.Namespace())] = fmt.Sprintf("Invalid HTTP method for \"%s\" field: \"%s\"", err.Field(), err.Value())
case "proxy_url":
errsStr[shortenNamespace(err.Namespace())] = fmt.Sprintf("Invalid proxy url for \"%s\" field: \"%s\" (it must be http, socks5 or socks5h)", err.Field(), err.Value())
case "string_bool":
errsStr[shortenNamespace(err.Namespace())] = fmt.Sprintf("Invalid value for \"%s\" field: \"%s\"", err.Field(), err.Value())
default:
errsStr[shortenNamespace(err.Namespace())] = fmt.Sprintf("Invalid value for \"%s\" field: \"%s\"", err.Field(), err.Value())
}
}
return NewValidationErrors(errsStr, errs)
}
func RequestErrorsFormater(err error) string {
switch e := err.(type) {
case *url.Error:
if netErr, ok := e.Err.(net.Error); ok && netErr.Timeout() {
return "Timeout Error"
}
if strings.Contains(e.Error(), "http: ContentLength=") {
println(e.Error())
return "Empty Body Error"
}
// TODO: Add more cases
}
return "Unknown Error"
}

28
go.mod
View File

@ -1,29 +1,21 @@
module github.com/aykhans/dodo module github.com/aykhans/dodo
go 1.23.2 go 1.24.0
require ( require (
github.com/go-playground/validator/v10 v10.23.0 github.com/jedib0t/go-pretty/v6 v6.6.7
github.com/jedib0t/go-pretty/v6 v6.6.2 github.com/valyala/fasthttp v1.60.0
github.com/spf13/cobra v1.8.1 gopkg.in/yaml.v3 v3.0.1
github.com/spf13/pflag v1.0.5
github.com/valyala/fasthttp v1.57.0
golang.org/x/net v0.31.0
) )
require ( require (
github.com/andybalholm/brotli v1.1.1 // indirect github.com/andybalholm/brotli v1.1.1 // indirect
github.com/gabriel-vasile/mimetype v1.4.4 // indirect github.com/klauspost/compress v1.18.0 // indirect
github.com/go-playground/locales v0.14.1 // indirect github.com/mattn/go-runewidth v0.0.16 // indirect
github.com/go-playground/universal-translator v0.18.1 // indirect
github.com/inconshreveable/mousetrap v1.1.0 // indirect
github.com/klauspost/compress v1.17.11 // indirect
github.com/leodido/go-urn v1.4.0 // indirect
github.com/mattn/go-runewidth v0.0.15 // indirect
github.com/rivo/uniseg v0.4.7 // indirect github.com/rivo/uniseg v0.4.7 // indirect
github.com/valyala/bytebufferpool v1.0.0 // indirect github.com/valyala/bytebufferpool v1.0.0 // indirect
golang.org/x/crypto v0.29.0 // indirect golang.org/x/net v0.38.0 // indirect
golang.org/x/sys v0.27.0 // indirect golang.org/x/sys v0.31.0 // indirect
golang.org/x/term v0.26.0 // indirect golang.org/x/term v0.30.0 // indirect
golang.org/x/text v0.20.0 // indirect golang.org/x/text v0.23.0 // indirect
) )

59
go.sum
View File

@ -1,56 +1,35 @@
github.com/andybalholm/brotli v1.1.1 h1:PR2pgnyFznKEugtsUo0xLdDop5SKXd5Qf5ysW+7XdTA= github.com/andybalholm/brotli v1.1.1 h1:PR2pgnyFznKEugtsUo0xLdDop5SKXd5Qf5ysW+7XdTA=
github.com/andybalholm/brotli v1.1.1/go.mod h1:05ib4cKhjx3OQYUY22hTVd34Bc8upXjOLL2rKwwZBoA= github.com/andybalholm/brotli v1.1.1/go.mod h1:05ib4cKhjx3OQYUY22hTVd34Bc8upXjOLL2rKwwZBoA=
github.com/cpuguy83/go-md2man/v2 v2.0.4/go.mod h1:tgQtvFlXSQOSOSIRvRPT7W67SCa46tRHOmNcaadrF8o=
github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c= github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c=
github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
github.com/gabriel-vasile/mimetype v1.4.4 h1:QjV6pZ7/XZ7ryI2KuyeEDE8wnh7fHP9YnQy+R0LnH8I= github.com/jedib0t/go-pretty/v6 v6.6.7 h1:m+LbHpm0aIAPLzLbMfn8dc3Ht8MW7lsSO4MPItz/Uuo=
github.com/gabriel-vasile/mimetype v1.4.4/go.mod h1:JwLei5XPtWdGiMFB5Pjle1oEeoSeEuJfJE+TtfvdB/s= github.com/jedib0t/go-pretty/v6 v6.6.7/go.mod h1:YwC5CE4fJ1HFUDeivSV1r//AmANFHyqczZk+U6BDALU=
github.com/go-playground/assert/v2 v2.2.0 h1:JvknZsQTYeFEAhQwI4qEt9cyV5ONwRHC+lYKSsYSR8s= github.com/klauspost/compress v1.18.0 h1:c/Cqfb0r+Yi+JtIEq73FWXVkRonBlf0CRNYc8Zttxdo=
github.com/go-playground/assert/v2 v2.2.0/go.mod h1:VDjEfimB/XKnb+ZQfWdccd7VUvScMdVu0Titje2rxJ4= github.com/klauspost/compress v1.18.0/go.mod h1:2Pp+KzxcywXVXMr50+X0Q/Lsb43OQHYWRCY2AiWywWQ=
github.com/go-playground/locales v0.14.1 h1:EWaQ/wswjilfKLTECiXz7Rh+3BjFhfDFKv/oXslEjJA= github.com/mattn/go-runewidth v0.0.16 h1:E5ScNMtiwvlvB5paMFdw9p4kSQzbXFikJ5SQO6TULQc=
github.com/go-playground/locales v0.14.1/go.mod h1:hxrqLVvrK65+Rwrd5Fc6F2O76J/NuW9t0sjnWqG1slY= github.com/mattn/go-runewidth v0.0.16/go.mod h1:Jdepj2loyihRzMpdS35Xk/zdY8IAYHsh153qUoGf23w=
github.com/go-playground/universal-translator v0.18.1 h1:Bcnm0ZwsGyWbCzImXv+pAJnYK9S473LQFuzCbDbfSFY=
github.com/go-playground/universal-translator v0.18.1/go.mod h1:xekY+UJKNuX9WP91TpwSH2VMlDf28Uj24BCp08ZFTUY=
github.com/go-playground/validator/v10 v10.23.0 h1:/PwmTwZhS0dPkav3cdK9kV1FsAmrL8sThn8IHr/sO+o=
github.com/go-playground/validator/v10 v10.23.0/go.mod h1:dbuPbCMFw/DrkbEynArYaCwl3amGuJotoKCe95atGMM=
github.com/inconshreveable/mousetrap v1.1.0 h1:wN+x4NVGpMsO7ErUn/mUI3vEoE6Jt13X2s0bqwp9tc8=
github.com/inconshreveable/mousetrap v1.1.0/go.mod h1:vpF70FUmC8bwa3OWnCshd2FqLfsEA9PFc4w1p2J65bw=
github.com/jedib0t/go-pretty/v6 v6.6.2 h1:27bLj3nRODzaiA7tPIxy9UVWHoPspFfME9XxgwiiNsM=
github.com/jedib0t/go-pretty/v6 v6.6.2/go.mod h1:zbn98qrYlh95FIhwwsbIip0LYpwSG8SUOScs+v9/t0E=
github.com/klauspost/compress v1.17.11 h1:In6xLpyWOi1+C7tXUUWv2ot1QvBjxevKAaI6IXrJmUc=
github.com/klauspost/compress v1.17.11/go.mod h1:pMDklpSncoRMuLFrf1W9Ss9KT+0rH90U12bZKk7uwG0=
github.com/leodido/go-urn v1.4.0 h1:WT9HwE9SGECu3lg4d/dIA+jxlljEa1/ffXKmRjqdmIQ=
github.com/leodido/go-urn v1.4.0/go.mod h1:bvxc+MVxLKB4z00jd1z+Dvzr47oO32F/QSNjSBOlFxI=
github.com/mattn/go-runewidth v0.0.15 h1:UNAjwbU9l54TA3KzvqLGxwWjHmMgBUVhBiTjelZgg3U=
github.com/mattn/go-runewidth v0.0.15/go.mod h1:Jdepj2loyihRzMpdS35Xk/zdY8IAYHsh153qUoGf23w=
github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM= github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM=
github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4=
github.com/rivo/uniseg v0.2.0/go.mod h1:J6wj4VEh+S6ZtnVlnTBMWIodfgj8LQOQFoIToxlJtxc= github.com/rivo/uniseg v0.2.0/go.mod h1:J6wj4VEh+S6ZtnVlnTBMWIodfgj8LQOQFoIToxlJtxc=
github.com/rivo/uniseg v0.4.7 h1:WUdvkW8uEhrYfLC4ZzdpI2ztxP1I582+49Oc5Mq64VQ= github.com/rivo/uniseg v0.4.7 h1:WUdvkW8uEhrYfLC4ZzdpI2ztxP1I582+49Oc5Mq64VQ=
github.com/rivo/uniseg v0.4.7/go.mod h1:FN3SvrM+Zdj16jyLfmOkMNblXMcoc8DfTHruCPUcx88= github.com/rivo/uniseg v0.4.7/go.mod h1:FN3SvrM+Zdj16jyLfmOkMNblXMcoc8DfTHruCPUcx88=
github.com/russross/blackfriday/v2 v2.1.0/go.mod h1:+Rmxgy9KzJVeS9/2gXHxylqXiyQDYRxCVz55jmeOWTM= github.com/stretchr/testify v1.10.0 h1:Xv5erBjTwe/5IxqUQTdXv5kgmIvbHo3QQyRwhJsOfJA=
github.com/spf13/cobra v1.8.1 h1:e5/vxKd/rZsfSJMUX1agtjeTDf+qv1/JdBF8gg5k9ZM= github.com/stretchr/testify v1.10.0/go.mod h1:r2ic/lqez/lEtzL7wO/rwa5dbSLXVDPFyf8C91i36aY=
github.com/spf13/cobra v1.8.1/go.mod h1:wHxEcudfqmLYa8iTfL+OuZPbBZkmvliBWKIezN3kD9Y=
github.com/spf13/pflag v1.0.5 h1:iy+VFUOCP1a+8yFto/drg2CJ5u0yRoB7fZw3DKv/JXA=
github.com/spf13/pflag v1.0.5/go.mod h1:McXfInJRrz4CZXVZOBLb0bTZqETkiAhM9Iw0y3An2Bg=
github.com/stretchr/testify v1.8.4 h1:CcVxjf3Q8PM0mHUKJCdn+eZZtm5yQwehR5yeSVQQcUk=
github.com/stretchr/testify v1.8.4/go.mod h1:sz/lmYIOXD/1dqDmKjjqLyZ2RngseejIcXlSw2iwfAo=
github.com/valyala/bytebufferpool v1.0.0 h1:GqA5TC/0021Y/b9FG4Oi9Mr3q7XYx6KllzawFIhcdPw= github.com/valyala/bytebufferpool v1.0.0 h1:GqA5TC/0021Y/b9FG4Oi9Mr3q7XYx6KllzawFIhcdPw=
github.com/valyala/bytebufferpool v1.0.0/go.mod h1:6bBcMArwyJ5K/AmCkWv1jt77kVWyCJ6HpOuEn7z0Csc= github.com/valyala/bytebufferpool v1.0.0/go.mod h1:6bBcMArwyJ5K/AmCkWv1jt77kVWyCJ6HpOuEn7z0Csc=
github.com/valyala/fasthttp v1.57.0 h1:Xw8SjWGEP/+wAAgyy5XTvgrWlOD1+TxbbvNADYCm1Tg= github.com/valyala/fasthttp v1.60.0 h1:kBRYS0lOhVJ6V+bYN8PqAHELKHtXqwq9zNMLKx1MBsw=
github.com/valyala/fasthttp v1.57.0/go.mod h1:h6ZBaPRlzpZ6O3H5t2gEk1Qi33+TmLvfwgLLp0t9CpE= github.com/valyala/fasthttp v1.60.0/go.mod h1:iY4kDgV3Gc6EqhRZ8icqcmlG6bqhcDXfuHgTO4FXCvc=
github.com/xyproto/randomstring v1.0.5 h1:YtlWPoRdgMu3NZtP45drfy1GKoojuR7hmRcnhZqKjWU= github.com/xyproto/randomstring v1.0.5 h1:YtlWPoRdgMu3NZtP45drfy1GKoojuR7hmRcnhZqKjWU=
github.com/xyproto/randomstring v1.0.5/go.mod h1:rgmS5DeNXLivK7YprL0pY+lTuhNQW3iGxZ18UQApw/E= github.com/xyproto/randomstring v1.0.5/go.mod h1:rgmS5DeNXLivK7YprL0pY+lTuhNQW3iGxZ18UQApw/E=
golang.org/x/crypto v0.29.0 h1:L5SG1JTTXupVV3n6sUqMTeWbjAyfPwoda2DLX8J8FrQ= golang.org/x/net v0.38.0 h1:vRMAPTMaeGqVhG5QyLJHqNDwecKTomGeqbnfZyKlBI8=
golang.org/x/crypto v0.29.0/go.mod h1:+F4F4N5hv6v38hfeYwTdx20oUvLLc+QfrE9Ax9HtgRg= golang.org/x/net v0.38.0/go.mod h1:ivrbrMbzFq5J41QOQh0siUuly180yBYtLp+CKbEaFx8=
golang.org/x/net v0.31.0 h1:68CPQngjLL0r2AlUKiSxtQFKvzRVbnzLwMUn5SzcLHo= golang.org/x/sys v0.31.0 h1:ioabZlmFYtWhL+TRYpcnNlLwhyxaM9kWTDEmfnprqik=
golang.org/x/net v0.31.0/go.mod h1:P4fl1q7dY2hnZFxEk4pPSkDHF+QqjitcnDjUQyMM+pM= golang.org/x/sys v0.31.0/go.mod h1:BJP2sWEmIv4KK5OTEluFJCKSidICx8ciO85XgH3Ak8k=
golang.org/x/sys v0.27.0 h1:wBqf8DvsY9Y/2P8gAfPDEYNuS30J4lPHJxXSb/nJZ+s= golang.org/x/term v0.30.0 h1:PQ39fJZ+mfadBm0y5WlL4vlM7Sx1Hgf13sMIY2+QS9Y=
golang.org/x/sys v0.27.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA= golang.org/x/term v0.30.0/go.mod h1:NYYFdzHoI5wRh/h5tDMdMqCqPJZEuNqVR5xJLd/n67g=
golang.org/x/term v0.26.0 h1:WEQa6V3Gja/BhNxg540hBip/kkaYtRg3cxg4oXSw4AU= golang.org/x/text v0.23.0 h1:D71I7dUrlY+VX0gQShAThNGHFxZ13dGLBHQLVl1mJlY=
golang.org/x/term v0.26.0/go.mod h1:Si5m1o57C5nBNQo5z1iq+XDijt21BDBDp2bK0QI8e3E= golang.org/x/text v0.23.0/go.mod h1:/BLNzu4aZCJ1+kcD0DNRotWKage4q2rGVAg4o22unh4=
golang.org/x/text v0.20.0 h1:gK/Kv2otX8gz+wn7Rmb3vT96ZwuoxnQlY+HlJVj7Qug= gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405 h1:yhCVgyC4o1eVCa2tZl7eS0r+SDo693bJlVdllGtEeKM=
golang.org/x/text v0.20.0/go.mod h1:D4IsuqiFMhST5bX19pQ9ikHC2GsaKyk/oF+pn3ducp4=
gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA= gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA=
gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=

113
main.go
View File

@ -2,117 +2,68 @@ package main
import ( import (
"context" "context"
"errors"
"fmt" "fmt"
"net/url"
"os" "os"
"os/signal" "os/signal"
"strings"
"syscall" "syscall"
"time"
"github.com/aykhans/dodo/config" "github.com/aykhans/dodo/config"
customerrors "github.com/aykhans/dodo/custom_errors"
"github.com/aykhans/dodo/readers"
"github.com/aykhans/dodo/requests" "github.com/aykhans/dodo/requests"
"github.com/aykhans/dodo/types"
"github.com/aykhans/dodo/utils" "github.com/aykhans/dodo/utils"
"github.com/aykhans/dodo/validation" "github.com/jedib0t/go-pretty/v6/text"
goValidator "github.com/go-playground/validator/v10"
) )
func main() { func main() {
validator := validation.NewValidator() conf := config.NewConfig()
conf := config.NewConfig("", 0, 0, 0, nil) configFile, err := conf.ReadCLI()
jsonConf := config.NewJSONConfig(
config.NewConfig("", 0, 0, 0, nil), nil, nil, nil, nil, nil,
)
cliConf, err := readers.CLIConfigReader()
if err != nil || 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 { if err != nil {
utils.PrintErrAndExit(err) utils.PrintErrAndExit(err)
} }
if err := validator.StructFiltered(
jsonConfNew,
func(ns []byte) bool {
return strings.LastIndex(string(ns), "Proxies") == -1
}); err != nil {
utils.PrintErrAndExit(
customerrors.ValidationErrorsFormater(
err.(goValidator.ValidationErrors),
),
)
}
jsonConf = jsonConfNew
conf.MergeConfigs(jsonConf.Config)
}
conf.MergeConfigs(cliConf.Config) if configFile.String() != "" {
tempConf := config.NewConfig()
if err := tempConf.ReadFile(configFile); err != nil {
utils.PrintErrAndExit(err)
}
tempConf.MergeConfig(conf)
conf = tempConf
}
conf.SetDefaults() conf.SetDefaults()
if err := validator.Struct(conf); err != nil {
utils.PrintErrAndExit( if errs := conf.Validate(); len(errs) > 0 {
customerrors.ValidationErrorsFormater( utils.PrintErrAndExit(errors.Join(errs...))
err.(goValidator.ValidationErrors),
),
)
} }
parsedURL, err := url.Parse(conf.URL) requestConf := config.NewRequestConfig(conf)
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,
NoProxyCheck: conf.NoProxyCheck.ValueOrPanic(),
}
requestConf.Print() requestConf.Print()
if !cliConf.Yes {
response := readers.CLIYesOrNoReader("Do you want to continue?", true) if !requestConf.Yes {
response := config.CLIYesOrNoReader("Do you want to continue?", false)
if !response { if !response {
utils.PrintAndExit("Exiting...") utils.PrintAndExit("Exiting...\n")
} }
fmt.Println()
} }
ctx, cancel := context.WithCancel(context.Background()) ctx, cancel := context.WithCancel(context.Background())
sigChan := make(chan os.Signal, 1) go listenForTermination(func() { cancel() })
signal.Notify(sigChan, syscall.SIGINT, syscall.SIGTERM)
go func() {
<-sigChan
cancel()
}()
responses, err := requests.Run(ctx, requestConf) responses, err := requests.Run(ctx, requestConf)
if err != nil { if err != nil {
if customerrors.Is(err, customerrors.ErrInterrupt) { if err == types.ErrInterrupt {
utils.PrintlnC(utils.Colors.Yellow, err.Error()) fmt.Println(text.FgYellow.Sprint(err.Error()))
return
} else if customerrors.Is(err, customerrors.ErrNoInternet) {
utils.PrintAndExit("No internet connection")
return return
} }
panic(err) utils.PrintErrAndExit(err)
} }
responses.Print() responses.Print()
} }
func listenForTermination(do func()) {
sigChan := make(chan os.Signal, 1)
signal.Notify(sigChan, syscall.SIGINT, syscall.SIGTERM)
<-sigChan
do()
}

View File

@ -1,96 +0,0 @@
package readers
import (
"fmt"
"github.com/aykhans/dodo/config"
"github.com/aykhans/dodo/custom_errors"
"github.com/aykhans/dodo/utils"
"github.com/spf13/cobra"
"github.com/spf13/pflag"
)
func CLIConfigReader() (*config.CLIConfig, error) {
var (
returnNil = false
cliConfig = config.NewCLIConfig(config.NewConfig("", 0, 0, 0, nil), false, "")
dodosCount uint
requestCount uint
timeout uint32
noProxyCheck bool
rootCmd = &cobra.Command{
Use: "dodo [flags]",
Example: ` Simple usage only with URL:
dodo -u https://example.com
Simple usage with config file:
dodo -c /path/to/config/file/config.json
Usage with all flags:
dodo -c /path/to/config/file/config.json -u https://example.com -m POST -d 10 -r 1000 -t 2000 --no-proxy-check -y`,
Run: func(cmd *cobra.Command, args []string) {},
SilenceErrors: true,
SilenceUsage: true,
Version: config.VERSION,
}
)
rootCmd.Flags().StringVarP(&cliConfig.ConfigFile, "config-file", "c", "", "Path to the config file")
rootCmd.Flags().BoolVarP(&cliConfig.Yes, "yes", "y", false, "Answer yes to all questions")
rootCmd.Flags().StringVarP(&cliConfig.Method, "method", "m", "", fmt.Sprintf("HTTP Method (default %s)", config.DefaultMethod))
rootCmd.Flags().StringVarP(&cliConfig.URL, "url", "u", "", "URL for stress testing")
rootCmd.Flags().UintVarP(&dodosCount, "dodos-count", "d", config.DefaultDodosCount, "Number of dodos(threads)")
rootCmd.Flags().UintVarP(&requestCount, "request-count", "r", config.DefaultRequestCount, "Number of total requests")
rootCmd.Flags().Uint32VarP(&timeout, "timeout", "t", config.DefaultTimeout, "Timeout for each request in milliseconds")
rootCmd.Flags().BoolVar(&noProxyCheck, "no-proxy-check", false, "Do not check for proxies")
if err := rootCmd.Execute(); err != nil {
utils.PrintErr(err)
rootCmd.Println(rootCmd.UsageString())
return nil, customerrors.CobraErrorFormater(err)
}
rootCmd.Flags().Visit(func(f *pflag.Flag) {
switch f.Name {
case "help":
returnNil = true
case "version":
returnNil = true
case "dodos-count":
cliConfig.DodosCount = dodosCount
case "request-count":
cliConfig.RequestCount = requestCount
case "timeout":
cliConfig.Timeout = timeout
case "no-proxy-check":
cliConfig.NoProxyCheck = utils.NewOption(noProxyCheck)
}
})
if returnNil {
return nil, nil
}
return cliConfig, nil
}
// CLIYesOrNoReader reads a yes or no answer from the command line.
// It prompts the user with the given message and default value,
// and returns true if the user answers "y" or "Y", and false otherwise.
// If there is an error while reading the input, it returns false.
// If the user simply presses enter without providing any input,
// it returns the default value specified by the `dft` parameter.
func CLIYesOrNoReader(message string, dft bool) bool {
var answer string
defaultMessage := "Y/n"
if !dft {
defaultMessage = "y/N"
}
fmt.Printf("%s [%s]: ", message, defaultMessage)
if _, err := fmt.Scanln(&answer); err != nil {
if err.Error() == "unexpected newline" {
return dft
}
return false
}
if answer == "" {
return dft
}
return answer == "y" || answer == "Y"
}

View File

@ -1,36 +0,0 @@
package readers
import (
"encoding/json"
"os"
"github.com/aykhans/dodo/config"
customerrors "github.com/aykhans/dodo/custom_errors"
)
func JSONConfigReader(filePath string) (*config.JSONConfig, error) {
data, err := os.ReadFile(filePath)
if err != nil {
return nil, customerrors.OSErrorFormater(err)
}
jsonConf := config.NewJSONConfig(
config.NewConfig("", 0, 0, 0, nil),
nil, nil, nil, nil, nil,
)
err = json.Unmarshal(data, &jsonConf)
if err != nil {
switch err := err.(type) {
case *json.UnmarshalTypeError:
return nil,
customerrors.NewTypeError(
err.Type.String(),
err.Value,
err.Field,
err,
)
}
return nil, customerrors.NewInvalidFileError(filePath, err)
}
return jsonConf, nil
}

View File

@ -2,14 +2,11 @@ package requests
import ( import (
"context" "context"
"fmt" "errors"
"math/rand" "math/rand"
"net/url" "net/url"
"sync"
"time" "time"
"github.com/aykhans/dodo/config"
"github.com/aykhans/dodo/readers"
"github.com/aykhans/dodo/utils" "github.com/aykhans/dodo/utils"
"github.com/valyala/fasthttp" "github.com/valyala/fasthttp"
"github.com/valyala/fasthttp/fasthttpproxy" "github.com/valyala/fasthttp/fasthttpproxy"
@ -22,18 +19,13 @@ type ClientGeneratorFunc func() *fasthttp.HostClient
func getClients( func getClients(
ctx context.Context, ctx context.Context,
timeout time.Duration, timeout time.Duration,
proxies []config.Proxy, proxies []url.URL,
dodosCount uint,
maxConns uint, maxConns uint,
yes bool, URL url.URL,
noProxyCheck bool,
URL *url.URL,
) []*fasthttp.HostClient { ) []*fasthttp.HostClient {
isTLS := URL.Scheme == "https" isTLS := URL.Scheme == "https"
if proxiesLen := len(proxies); proxiesLen > 0 { if proxiesLen := len(proxies); proxiesLen > 0 {
// If noProxyCheck is true, we will return the clients without checking the proxies.
if noProxyCheck {
clients := make([]*fasthttp.HostClient, 0, proxiesLen) clients := make([]*fasthttp.HostClient, 0, proxiesLen)
addr := URL.Host addr := URL.Host
if isTLS && URL.Port() == "" { if isTLS && URL.Port() == "" {
@ -61,43 +53,6 @@ func getClients(
return clients return clients
} }
// Else, we will check the proxies and return the active ones.
activeProxyClients := getActiveProxyClients(
ctx, proxies, timeout, dodosCount, maxConns, URL,
)
if ctx.Err() != nil {
return nil
}
activeProxyClientsCount := uint(len(activeProxyClients))
var yesOrNoMessage string
var yesOrNoDefault bool
if activeProxyClientsCount == 0 {
yesOrNoDefault = false
yesOrNoMessage = utils.Colored(
utils.Colors.Yellow,
"No active proxies found. Do you want to continue?",
)
} else {
yesOrNoMessage = utils.Colored(
utils.Colors.Yellow,
fmt.Sprintf(
"Found %d active proxies. Do you want to continue?",
activeProxyClientsCount,
),
)
}
if !yes {
response := readers.CLIYesOrNoReader("\n"+yesOrNoMessage, yesOrNoDefault)
if !response {
utils.PrintAndExit("Exiting...")
}
}
fmt.Println()
if activeProxyClientsCount > 0 {
return activeProxyClients
}
}
client := &fasthttp.HostClient{ client := &fasthttp.HostClient{
MaxConns: int(maxConns), MaxConns: int(maxConns),
IsTLS: isTLS, IsTLS: isTLS,
@ -110,200 +65,20 @@ func getClients(
return []*fasthttp.HostClient{client} return []*fasthttp.HostClient{client}
} }
// getActiveProxyClients divides the proxies into slices based on the number of dodos and // getDialFunc returns the appropriate fasthttp.DialFunc based on the provided proxy URL scheme.
// launches goroutines to find active proxy clients for each slice. // It supports SOCKS5 ('socks5' or 'socks5h') and HTTP ('http') proxy schemes.
// It uses a progress tracker to monitor the progress of the search. // For HTTP proxies, the timeout parameter determines connection timeouts.
// Once all goroutines have completed, the function waits for them to finish and // Returns an error if the proxy scheme is unsupported.
// returns a flattened slice of active proxy clients. func getDialFunc(proxy *url.URL, timeout time.Duration) (fasthttp.DialFunc, error) {
func getActiveProxyClients(
ctx context.Context,
proxies []config.Proxy,
timeout time.Duration,
dodosCount uint,
maxConns uint,
URL *url.URL,
) []*fasthttp.HostClient {
activeProxyClientsArray := make([][]*fasthttp.HostClient, dodosCount)
proxiesCount := len(proxies)
dodosCountInt := int(dodosCount)
var (
wg sync.WaitGroup
streamWG sync.WaitGroup
)
wg.Add(dodosCountInt)
streamWG.Add(1)
var proxiesSlice []config.Proxy
increase := make(chan int64, proxiesCount)
streamCtx, streamCtxCancel := context.WithCancel(context.Background())
go streamProgress(streamCtx, &streamWG, int64(proxiesCount), "Searching for active proxies🌐", increase)
for i := range dodosCountInt {
if i+1 == dodosCountInt {
proxiesSlice = proxies[i*proxiesCount/dodosCountInt:]
} else {
proxiesSlice = proxies[i*proxiesCount/dodosCountInt : (i+1)*proxiesCount/dodosCountInt]
}
go findActiveProxyClients(
ctx,
proxiesSlice,
timeout,
&activeProxyClientsArray[i],
increase,
maxConns,
URL,
&wg,
)
}
wg.Wait()
streamCtxCancel()
streamWG.Wait()
return utils.Flatten(activeProxyClientsArray)
}
// findActiveProxyClients checks a list of proxies to determine which ones are active
// and appends the active ones to the provided activeProxyClients slice.
//
// Parameters:
// - ctx: The context to control cancellation and timeout.
// - proxies: A slice of Proxy configurations to be checked.
// - timeout: The duration to wait for each proxy check before timing out.
// - activeProxyClients: A pointer to a slice where active proxy clients will be appended.
// - increase: A channel to signal the increase of checked proxies count.
// - URL: The URL to be used for checking the proxies.
// - wg: A WaitGroup to signal when the function is done.
//
// The function sends a GET request to each proxy using the provided URL. If the proxy
// responds with a status code of 200, it is considered active and added to the activeProxyClients slice.
// The function respects the context's cancellation and timeout settings.
func findActiveProxyClients(
ctx context.Context,
proxies []config.Proxy,
timeout time.Duration,
activeProxyClients *[]*fasthttp.HostClient,
increase chan<- int64,
maxConns uint,
URL *url.URL,
wg *sync.WaitGroup,
) {
defer wg.Done()
request := fasthttp.AcquireRequest()
defer fasthttp.ReleaseRequest(request)
request.SetRequestURI(config.ProxyCheckURL)
request.Header.SetMethod("GET")
for _, proxy := range proxies {
if ctx.Err() != nil {
return
}
func() {
defer func() { increase <- 1 }()
response := fasthttp.AcquireResponse()
defer fasthttp.ReleaseResponse(response)
dialFunc, err := getDialFunc(&proxy, timeout)
if err != nil {
return
}
client := &fasthttp.Client{
Dial: dialFunc,
}
defer client.CloseIdleConnections()
ch := make(chan error)
go func() {
err := client.DoTimeout(request, response, timeout)
ch <- err
}()
select {
case err := <-ch:
if err != nil {
return
}
break
case <-time.After(timeout):
return
case <-ctx.Done():
return
}
isTLS := URL.Scheme == "https"
addr := URL.Host
if isTLS && URL.Port() == "" {
addr += ":443"
}
if response.StatusCode() == 200 {
*activeProxyClients = append(
*activeProxyClients,
&fasthttp.HostClient{
MaxConns: int(maxConns),
IsTLS: isTLS,
Addr: addr,
Dial: dialFunc,
MaxIdleConnDuration: timeout,
MaxConnDuration: timeout,
WriteTimeout: timeout,
ReadTimeout: timeout,
},
)
}
}()
}
}
// getDialFunc returns a fasthttp.DialFunc based on the provided proxy configuration.
// It takes a pointer to a config.Proxy struct as input and returns a fasthttp.DialFunc and an error.
// The function parses the proxy URL, determines the scheme (socks5, socks5h, http, or https),
// and creates a dialer accordingly. If the proxy URL is invalid or the scheme is not supported,
// it returns an error.
func getDialFunc(proxy *config.Proxy, timeout time.Duration) (fasthttp.DialFunc, error) {
parsedProxyURL, err := url.Parse(proxy.URL)
if err != nil {
return nil, err
}
var dialer fasthttp.DialFunc var dialer fasthttp.DialFunc
if parsedProxyURL.Scheme == "socks5" || parsedProxyURL.Scheme == "socks5h" {
if proxy.Username != "" { switch proxy.Scheme {
dialer = fasthttpproxy.FasthttpSocksDialer( case "socks5", "socks5h":
fmt.Sprintf( dialer = fasthttpproxy.FasthttpSocksDialerDualStack(proxy.String())
"%s://%s:%s@%s", case "http":
parsedProxyURL.Scheme, dialer = fasthttpproxy.FasthttpHTTPDialerDualStackTimeout(proxy.String(), timeout)
proxy.Username, default:
proxy.Password, return nil, errors.New("unsupported proxy scheme")
parsedProxyURL.Host,
),
)
} else {
dialer = fasthttpproxy.FasthttpSocksDialer(
fmt.Sprintf(
"%s://%s",
parsedProxyURL.Scheme,
parsedProxyURL.Host,
),
)
}
} else if parsedProxyURL.Scheme == "http" {
if proxy.Username != "" {
dialer = fasthttpproxy.FasthttpHTTPDialerTimeout(
fmt.Sprintf(
"%s:%s@%s",
proxy.Username, proxy.Password, parsedProxyURL.Host,
),
timeout,
)
} else {
dialer = fasthttpproxy.FasthttpHTTPDialerTimeout(
parsedProxyURL.Host,
timeout,
)
}
} else {
return nil, err
} }
return dialer, nil return dialer, nil
} }

View File

@ -7,7 +7,6 @@ import (
"time" "time"
"github.com/jedib0t/go-pretty/v6/progress" "github.com/jedib0t/go-pretty/v6/progress"
"github.com/valyala/fasthttp"
) )
// streamProgress streams the progress of a task to the console using a progress bar. // streamProgress streams the progress of a task to the console using a progress bar.
@ -18,7 +17,7 @@ import (
func streamProgress( func streamProgress(
ctx context.Context, ctx context.Context,
wg *sync.WaitGroup, wg *sync.WaitGroup,
total int64, total uint,
message string, message string,
increase <-chan int64, increase <-chan int64,
) { ) {
@ -28,19 +27,26 @@ func streamProgress(
pw.SetStyle(progress.StyleBlocks) pw.SetStyle(progress.StyleBlocks)
pw.SetTrackerLength(40) pw.SetTrackerLength(40)
pw.SetUpdateFrequency(time.Millisecond * 250) pw.SetUpdateFrequency(time.Millisecond * 250)
if total == 0 {
pw.Style().Visibility.Percentage = false
}
go pw.Render() go pw.Render()
dodosTracker := progress.Tracker{ dodosTracker := progress.Tracker{
Message: message, Message: message,
Total: total, Total: int64(total),
} }
pw.AppendTracker(&dodosTracker) pw.AppendTracker(&dodosTracker)
for { for {
select { select {
case <-ctx.Done(): case <-ctx.Done():
fmt.Printf("\r") if err := ctx.Err(); err == context.Canceled || err == context.DeadlineExceeded {
dodosTracker.MarkAsDone()
} else {
dodosTracker.MarkAsErrored() dodosTracker.MarkAsErrored()
}
time.Sleep(time.Millisecond * 300) time.Sleep(time.Millisecond * 300)
pw.Stop() fmt.Printf("\r")
return return
case value := <-increase: case value := <-increase:
@ -48,28 +54,3 @@ func streamProgress(
} }
} }
} }
// checkConnection checks the internet connection by making requests to different websites.
// It returns true if the connection is successful, otherwise false.
func checkConnection(ctx context.Context) bool {
ch := make(chan bool)
go func() {
_, _, err := fasthttp.Get(nil, "https://www.google.com")
if err != nil {
_, _, err = fasthttp.Get(nil, "https://www.bing.com")
if err != nil {
_, _, err = fasthttp.Get(nil, "https://www.yahoo.com")
ch <- err == nil
}
ch <- true
}
ch <- true
}()
select {
case <-ctx.Done():
return false
case res := <-ch:
return res
}
}

View File

@ -7,7 +7,7 @@ import (
"time" "time"
"github.com/aykhans/dodo/config" "github.com/aykhans/dodo/config"
customerrors "github.com/aykhans/dodo/custom_errors" "github.com/aykhans/dodo/types"
"github.com/aykhans/dodo/utils" "github.com/aykhans/dodo/utils"
"github.com/valyala/fasthttp" "github.com/valyala/fasthttp"
) )
@ -26,7 +26,6 @@ type Request struct {
func (r *Request) Send(ctx context.Context, timeout time.Duration) (*fasthttp.Response, error) { func (r *Request) Send(ctx context.Context, timeout time.Duration) (*fasthttp.Response, error) {
client := r.getClient() client := r.getClient()
request := r.getRequest() request := r.getRequest()
defer client.CloseIdleConnections()
defer fasthttp.ReleaseRequest(request) defer fasthttp.ReleaseRequest(request)
response := fasthttp.AcquireResponse() response := fasthttp.AcquireResponse()
@ -44,9 +43,9 @@ func (r *Request) Send(ctx context.Context, timeout time.Duration) (*fasthttp.Re
return response, nil return response, nil
case <-time.After(timeout): case <-time.After(timeout):
fasthttp.ReleaseResponse(response) fasthttp.ReleaseResponse(response)
return nil, customerrors.ErrTimeout return nil, types.ErrTimeout
case <-ctx.Done(): case <-ctx.Done():
return nil, customerrors.ErrInterrupt return nil, types.ErrInterrupt
} }
} }
@ -75,9 +74,9 @@ func newRequest(
getRequest := getRequestGeneratorFunc( getRequest := getRequestGeneratorFunc(
requestConfig.URL, requestConfig.URL,
requestConfig.Params,
requestConfig.Headers, requestConfig.Headers,
requestConfig.Cookies, requestConfig.Cookies,
requestConfig.Params,
requestConfig.Method, requestConfig.Method,
requestConfig.Body, requestConfig.Body,
localRand, localRand,
@ -91,37 +90,36 @@ func newRequest(
return requests return requests
} }
// getRequestGeneratorFunc returns a RequestGeneratorFunc which generates HTTP requests // getRequestGeneratorFunc returns a RequestGeneratorFunc which generates HTTP requests with the specified parameters.
// with the specified parameters. // The function uses a local random number generator to select bodies, headers, cookies, and parameters if multiple options are provided.
// The function uses a local random number generator to select bodies, headers, cookies, and parameters
// if multiple options are provided.
func getRequestGeneratorFunc( func getRequestGeneratorFunc(
URL *url.URL, URL url.URL,
Headers map[string][]string, params types.Params,
Cookies map[string][]string, headers types.Headers,
Params map[string][]string, cookies types.Cookies,
Method string, method string,
Bodies []string, bodies []string,
localRand *rand.Rand, localRand *rand.Rand,
) RequestGeneratorFunc { ) RequestGeneratorFunc {
bodiesLen := len(Bodies) bodiesLen := len(bodies)
getBody := func() string { return "" } getBody := func() string { return "" }
if bodiesLen == 1 { if bodiesLen == 1 {
getBody = func() string { return Bodies[0] } getBody = func() string { return bodies[0] }
} else if bodiesLen > 1 { } else if bodiesLen > 1 {
getBody = utils.RandomValueCycle(Bodies, localRand) getBody = utils.RandomValueCycle(bodies, localRand)
} }
getHeaders := getKeyValueSetFunc(Headers, localRand)
getCookies := getKeyValueSetFunc(Cookies, localRand) getParams := getKeyValueGeneratorFunc(params, localRand)
getParams := getKeyValueSetFunc(Params, localRand) getHeaders := getKeyValueGeneratorFunc(headers, localRand)
getCookies := getKeyValueGeneratorFunc(cookies, localRand)
return func() *fasthttp.Request { return func() *fasthttp.Request {
return newFasthttpRequest( return newFasthttpRequest(
URL, URL,
getParams(),
getHeaders(), getHeaders(),
getCookies(), getCookies(),
getParams(), method,
Method,
getBody(), getBody(),
) )
} }
@ -130,12 +128,12 @@ func getRequestGeneratorFunc(
// newFasthttpRequest creates a new fasthttp.Request object with the provided parameters. // newFasthttpRequest creates a new fasthttp.Request object with the provided parameters.
// It sets the request URI, host header, headers, cookies, params, method, and body. // It sets the request URI, host header, headers, cookies, params, method, and body.
func newFasthttpRequest( func newFasthttpRequest(
URL *url.URL, URL url.URL,
Headers map[string]string, params []types.KeyValue[string, string],
Cookies map[string]string, headers []types.KeyValue[string, string],
Params map[string]string, cookies []types.KeyValue[string, string],
Method string, method string,
Body string, body string,
) *fasthttp.Request { ) *fasthttp.Request {
request := fasthttp.AcquireRequest() request := fasthttp.AcquireRequest()
request.SetRequestURI(URL.Path) request.SetRequestURI(URL.Path)
@ -143,12 +141,12 @@ func newFasthttpRequest(
// Set the host of the request to the host header // Set the host of the request to the host header
// If the host header is not set, the request will fail // If the host header is not set, the request will fail
// If there is host header in the headers, it will be overwritten // If there is host header in the headers, it will be overwritten
request.Header.Set("Host", URL.Host) request.Header.SetHost(URL.Host)
setRequestHeaders(request, Headers) setRequestParams(request, params)
setRequestCookies(request, Cookies) setRequestHeaders(request, headers)
setRequestParams(request, Params) setRequestCookies(request, cookies)
setRequestMethod(request, Method) setRequestMethod(request, method)
setRequestBody(request, Body) setRequestBody(request, body)
if URL.Scheme == "https" { if URL.Scheme == "https" {
request.URI().SetScheme("https") request.URI().SetScheme("https")
} }
@ -156,28 +154,25 @@ func newFasthttpRequest(
return request return request
} }
// setRequestHeaders sets the headers of the given request with the provided key-value pairs. // setRequestParams adds the query parameters of the given request based on the provided key-value pairs.
func setRequestHeaders(req *fasthttp.Request, headers map[string]string) { func setRequestParams(req *fasthttp.Request, params []types.KeyValue[string, string]) {
req.Header.Set("User-Agent", config.DefaultUserAgent) for _, param := range params {
for key, value := range headers { req.URI().QueryArgs().Add(param.Key, param.Value)
req.Header.Set(key, value)
} }
} }
// setRequestCookies sets the cookies in the given request. // setRequestHeaders adds the headers of the given request with the provided key-value pairs.
func setRequestCookies(req *fasthttp.Request, cookies map[string]string) { func setRequestHeaders(req *fasthttp.Request, headers []types.KeyValue[string, string]) {
for key, value := range cookies { for _, header := range headers {
req.Header.SetCookie(key, value) req.Header.Add(header.Key, header.Value)
} }
} }
// setRequestParams sets the query parameters of the given request based on the provided map of key-value pairs. // setRequestCookies adds the cookies of the given request with the provided key-value pairs.
func setRequestParams(req *fasthttp.Request, params map[string]string) { func setRequestCookies(req *fasthttp.Request, cookies []types.KeyValue[string, string]) {
urlParams := url.Values{} for _, cookie := range cookies {
for key, value := range params { req.Header.Add("Cookie", cookie.Key+"="+cookie.Value)
urlParams.Add(key, value)
} }
req.URI().SetQueryString(urlParams.Encode())
} }
// setRequestMethod sets the HTTP request method for the given request. // setRequestMethod sets the HTTP request method for the given request.
@ -191,59 +186,62 @@ func setRequestBody(req *fasthttp.Request, body string) {
req.SetBody([]byte(body)) req.SetBody([]byte(body))
} }
// getKeyValueSetFunc generates a function that returns a map of key-value pairs based on the provided key-value set. // getKeyValueGeneratorFunc creates a function that generates key-value pairs for HTTP requests.
// The generated function will either return fixed values or random values depending on the input. // It takes a slice of key-value pairs where each key maps to a slice of possible values,
// and a random number generator.
// //
// Returns: // If any key has multiple possible values, the function will randomly select one value for each
// - A function that returns a map of key-value pairs. If the input map contains multiple values for a key, // call (using the provided random number generator). If all keys have at most one value, the
// the returned function will generate random values for that key. If the input map contains a single value // function will always return the same set of key-value pairs for efficiency.
// for a key, the returned function will always return that value. If the input map is empty for a key, func getKeyValueGeneratorFunc[
// the returned function will generate an empty string for that key. T []types.KeyValue[string, string],
func getKeyValueSetFunc[ ](
KeyValueSet map[string][]string, keyValueSlice []types.KeyValue[string, []string],
KeyValue map[string]string, localRand *rand.Rand,
](keyValueSet KeyValueSet, localRand *rand.Rand) func() KeyValue { ) func() T {
getKeyValueSlice := []map[string]func() string{} getKeyValueSlice := []map[string]func() string{}
isRandom := false isRandom := false
for key, values := range keyValueSet {
valuesLen := len(values)
// if values is empty, return a function that generates empty string for _, kv := range keyValueSlice {
// if values has only one element, return a function that generates that element valuesLen := len(kv.Value)
// if values has more than one element, return a function that generates a random element
getKeyValue := func() string { return "" } getValueFunc := func() string { return "" }
if valuesLen == 1 { if valuesLen == 1 {
getKeyValue = func() string { return values[0] } getValueFunc = func() string { return kv.Value[0] }
} else if valuesLen > 1 { } else if valuesLen > 1 {
getKeyValue = utils.RandomValueCycle(values, localRand) getValueFunc = utils.RandomValueCycle(kv.Value, localRand)
isRandom = true isRandom = true
} }
getKeyValueSlice = append( getKeyValueSlice = append(
getKeyValueSlice, getKeyValueSlice,
map[string]func() string{key: getKeyValue}, map[string]func() string{kv.Key: getValueFunc},
) )
} }
// if isRandom is true, return a function that generates random values,
// otherwise return a function that generates fixed values to avoid unnecessary random number generation
if isRandom { if isRandom {
return func() KeyValue { return func() T {
keyValues := make(KeyValue, len(getKeyValueSlice)) keyValues := make(T, len(getKeyValueSlice))
for _, keyValue := range getKeyValueSlice { for i, keyValue := range getKeyValueSlice {
for key, value := range keyValue { for key, value := range keyValue {
keyValues[key] = value() keyValues[i] = types.KeyValue[string, string]{
Key: key,
Value: value(),
}
} }
} }
return keyValues return keyValues
} }
} else { } else {
keyValues := make(KeyValue, len(getKeyValueSlice)) keyValues := make(T, len(getKeyValueSlice))
for _, keyValue := range getKeyValueSlice { for i, keyValue := range getKeyValueSlice {
for key, value := range keyValue { for key, value := range keyValue {
keyValues[key] = value() keyValues[i] = types.KeyValue[string, string]{
Key: key,
Value: value(),
} }
} }
return func() KeyValue { return keyValues } }
return func() T { return keyValues }
} }
} }

View File

@ -1,85 +1,108 @@
package requests package requests
import ( import (
"fmt"
"os" "os"
"time" "time"
"github.com/aykhans/dodo/types"
"github.com/aykhans/dodo/utils" "github.com/aykhans/dodo/utils"
"github.com/jedib0t/go-pretty/v6/table" "github.com/jedib0t/go-pretty/v6/table"
) )
type Response struct { type Response struct {
StatusCode int Response string
Error error
Time time.Duration Time time.Duration
} }
type Responses []*Response type Responses []*Response
// Print prints the responses in a tabular format, including information such as // Print prints the responses in a tabular format, including information such as
// response count, minimum time, maximum time, and average time. // response count, minimum time, maximum time, average time, and latency percentiles.
func (respones Responses) Print() { func (responses Responses) Print() {
var ( total := struct {
totalMinDuration time.Duration = respones[0].Time Count int
totalMaxDuration time.Duration = respones[0].Time Min time.Duration
totalDuration time.Duration Max time.Duration
totalCount int = len(respones) Sum time.Duration
) P90 time.Duration
mergedResponses := make(map[string][]time.Duration) P95 time.Duration
P99 time.Duration
for _, response := range respones { }{
if response.Time < totalMinDuration { Count: len(responses),
totalMinDuration = response.Time Min: responses[0].Time,
Max: responses[0].Time,
} }
if response.Time > totalMaxDuration { mergedResponses := make(map[string]types.Durations)
totalMaxDuration = response.Time var allDurations types.Durations
}
totalDuration += response.Time
if response.Error != nil { for _, response := range responses {
mergedResponses[response.Error.Error()] = append( if response.Time < total.Min {
mergedResponses[response.Error.Error()], total.Min = response.Time
response.Time, }
) if response.Time > total.Max {
} else { total.Max = response.Time
mergedResponses[fmt.Sprintf("%d", response.StatusCode)] = append( }
mergedResponses[fmt.Sprintf("%d", response.StatusCode)], total.Sum += response.Time
mergedResponses[response.Response] = append(
mergedResponses[response.Response],
response.Time, response.Time,
) )
allDurations = append(allDurations, response.Time)
} }
} allDurations.Sort()
allDurationsLenAsFloat := float64(len(allDurations) - 1)
total.P90 = allDurations[int(0.90*allDurationsLenAsFloat)]
total.P95 = allDurations[int(0.95*allDurationsLenAsFloat)]
total.P99 = allDurations[int(0.99*allDurationsLenAsFloat)]
t := table.NewWriter() t := table.NewWriter()
t.SetOutputMirror(os.Stdout) t.SetOutputMirror(os.Stdout)
t.SetStyle(table.StyleLight) t.SetStyle(table.StyleLight)
t.SetColumnConfigs([]table.ColumnConfig{ t.SetColumnConfigs([]table.ColumnConfig{
{Number: 1, WidthMax: 80}, {Number: 1, WidthMax: 40},
}) })
t.AppendHeader(table.Row{ t.AppendHeader(table.Row{
"Response", "Response",
"Count", "Count",
"Min Time", "Min",
"Max Time", "Max",
"Average Time", "Average",
"P90",
"P95",
"P99",
}) })
var roundPrecision int64 = 4
for key, durations := range mergedResponses { for key, durations := range mergedResponses {
durations.Sort()
durationsLen := len(durations)
durationsLenAsFloat := float64(durationsLen - 1)
t.AppendRow(table.Row{ t.AppendRow(table.Row{
key, key,
len(durations), durationsLen,
utils.MinDuration(durations...), utils.DurationRoundBy(*durations.First(), roundPrecision),
utils.MaxDuration(durations...), utils.DurationRoundBy(*durations.Last(), roundPrecision),
utils.AvgDuration(durations...), utils.DurationRoundBy(durations.Avg(), roundPrecision),
utils.DurationRoundBy(durations[int(0.90*durationsLenAsFloat)], roundPrecision),
utils.DurationRoundBy(durations[int(0.95*durationsLenAsFloat)], roundPrecision),
utils.DurationRoundBy(durations[int(0.99*durationsLenAsFloat)], roundPrecision),
}) })
t.AppendSeparator() t.AppendSeparator()
} }
if len(mergedResponses) > 1 {
t.AppendRow(table.Row{ t.AppendRow(table.Row{
"Total", "Total",
totalCount, total.Count,
totalMinDuration, utils.DurationRoundBy(total.Min, roundPrecision),
totalMaxDuration, utils.DurationRoundBy(total.Max, roundPrecision),
totalDuration / time.Duration(totalCount), utils.DurationRoundBy(total.Sum/time.Duration(total.Count), roundPrecision), // Average
utils.DurationRoundBy(total.P90, roundPrecision),
utils.DurationRoundBy(total.P95, roundPrecision),
utils.DurationRoundBy(total.P99, roundPrecision),
}) })
}
t.Render() t.Render()
} }

View File

@ -2,52 +2,44 @@ package requests
import ( import (
"context" "context"
"strconv"
"sync" "sync"
"time" "time"
"github.com/aykhans/dodo/config" "github.com/aykhans/dodo/config"
customerrors "github.com/aykhans/dodo/custom_errors" "github.com/aykhans/dodo/types"
"github.com/aykhans/dodo/utils" "github.com/aykhans/dodo/utils"
"github.com/valyala/fasthttp" "github.com/valyala/fasthttp"
) )
// Run executes the main logic for processing requests based on the provided configuration. // Run executes the main logic for processing requests based on the provided configuration.
// It first checks for an internet connection with a timeout context. If no connection is found, // It initializes clients based on the request configuration and releases the dodos.
// it returns an error. Then, it initializes clients based on the request configuration and // If the context is canceled and no responses are collected, it returns an interrupt error.
// releases the dodos. If the context is canceled and no responses are collected, it returns an interrupt error.
// //
// Parameters: // Parameters:
// - ctx: The context for managing request lifecycle and cancellation. // - ctx: The context for managing request lifecycle and cancellation.
// - requestConfig: The configuration for the request, including timeout, proxies, and other settings. // - requestConfig: The configuration for the request, including timeout, proxies, and other settings.
//
// Returns:
// - Responses: A collection of responses from the executed requests.
// - error: An error if the operation fails, such as no internet connection or an interrupt.
func Run(ctx context.Context, requestConfig *config.RequestConfig) (Responses, error) { func Run(ctx context.Context, requestConfig *config.RequestConfig) (Responses, error) {
checkConnectionCtx, checkConnectionCtxCancel := context.WithTimeout(ctx, 8*time.Second) if requestConfig.Duration > 0 {
if !checkConnection(checkConnectionCtx) { var cancel context.CancelFunc
checkConnectionCtxCancel() ctx, cancel = context.WithTimeout(ctx, requestConfig.Duration)
return nil, customerrors.ErrNoInternet defer cancel()
} }
checkConnectionCtxCancel()
clients := getClients( clients := getClients(
ctx, ctx,
requestConfig.Timeout, requestConfig.Timeout,
requestConfig.Proxies, requestConfig.Proxies,
requestConfig.GetValidDodosCountForProxies(),
requestConfig.GetMaxConns(fasthttp.DefaultMaxConnsPerHost), requestConfig.GetMaxConns(fasthttp.DefaultMaxConnsPerHost),
requestConfig.Yes,
requestConfig.NoProxyCheck,
requestConfig.URL, requestConfig.URL,
) )
if clients == nil { if clients == nil {
return nil, customerrors.ErrInterrupt return nil, types.ErrInterrupt
} }
responses := releaseDodos(ctx, requestConfig, clients) responses := releaseDodos(ctx, requestConfig, clients)
if ctx.Err() != nil && len(responses) == 0 { if ctx.Err() != nil && len(responses) == 0 {
return nil, customerrors.ErrInterrupt return nil, types.ErrInterrupt
} }
return responses, nil return responses, nil
@ -72,28 +64,38 @@ func releaseDodos(
wg sync.WaitGroup wg sync.WaitGroup
streamWG sync.WaitGroup streamWG sync.WaitGroup
requestCountPerDodo uint requestCountPerDodo uint
dodosCount uint = requestConfig.GetValidDodosCountForRequests() dodosCount = requestConfig.GetValidDodosCountForRequests()
dodosCountInt int = int(dodosCount)
requestCount uint = uint(requestConfig.RequestCount)
responses = make([][]*Response, dodosCount) responses = make([][]*Response, dodosCount)
increase = make(chan int64, requestCount) increase = make(chan int64, requestConfig.RequestCount)
) )
wg.Add(dodosCountInt) wg.Add(int(dodosCount))
streamWG.Add(1) streamWG.Add(1)
streamCtx, streamCtxCancel := context.WithCancel(context.Background()) streamCtx, streamCtxCancel := context.WithCancel(context.Background())
go streamProgress(streamCtx, &streamWG, int64(requestCount), "Dodos Working🔥", increase) go streamProgress(streamCtx, &streamWG, requestConfig.RequestCount, "Dodos Working🔥", increase)
if requestConfig.RequestCount == 0 {
for i := range dodosCount {
go sendRequest(
ctx,
newRequest(*requestConfig, clients, int64(i)),
requestConfig.Timeout,
&responses[i],
increase,
&wg,
)
}
} else {
for i := range dodosCount { for i := range dodosCount {
if i+1 == dodosCount { if i+1 == dodosCount {
requestCountPerDodo = requestCount - (i * requestCount / dodosCount) requestCountPerDodo = requestConfig.RequestCount - (i * requestConfig.RequestCount / dodosCount)
} else { } else {
requestCountPerDodo = ((i + 1) * requestCount / dodosCount) - requestCountPerDodo = ((i + 1) * requestConfig.RequestCount / dodosCount) -
(i * requestCount / dodosCount) (i * requestConfig.RequestCount / dodosCount)
} }
go sendRequest( go sendRequestByCount(
ctx, ctx,
newRequest(*requestConfig, clients, int64(i)), newRequest(*requestConfig, clients, int64(i)),
requestConfig.Timeout, requestConfig.Timeout,
@ -103,17 +105,19 @@ func releaseDodos(
&wg, &wg,
) )
} }
}
wg.Wait() wg.Wait()
streamCtxCancel() streamCtxCancel()
streamWG.Wait() streamWG.Wait()
return utils.Flatten(responses) return utils.Flatten(responses)
} }
// sendRequest sends a specified number of HTTP requests concurrently with a given timeout. // sendRequestByCount sends a specified number of HTTP requests concurrently with a given timeout.
// It appends the responses to the provided responseData slice and sends the count of completed requests // It appends the responses to the provided responseData slice and sends the count of completed requests
// to the increase channel. The function terminates early if the context is canceled or if a custom // to the increase channel. The function terminates early if the context is canceled or if a custom
// interrupt error is encountered. // interrupt error is encountered.
func sendRequest( func sendRequestByCount(
ctx context.Context, ctx context.Context,
request *Request, request *Request,
timeout time.Duration, timeout time.Duration,
@ -138,12 +142,11 @@ func sendRequest(
} }
if err != nil { if err != nil {
if err == customerrors.ErrInterrupt { if err == types.ErrInterrupt {
return return
} }
*responseData = append(*responseData, &Response{ *responseData = append(*responseData, &Response{
StatusCode: 0, Response: err.Error(),
Error: err,
Time: completedTime, Time: completedTime,
}) })
increase <- 1 increase <- 1
@ -151,8 +154,54 @@ func sendRequest(
} }
*responseData = append(*responseData, &Response{ *responseData = append(*responseData, &Response{
StatusCode: response.StatusCode(), Response: strconv.Itoa(response.StatusCode()),
Error: nil, Time: completedTime,
})
increase <- 1
}()
}
}
// sendRequest continuously sends HTTP requests until the context is canceled.
// It records the response status code or error message along with the response time,
// and signals each completed request through the increase channel.
func sendRequest(
ctx context.Context,
request *Request,
timeout time.Duration,
responseData *[]*Response,
increase chan<- int64,
wg *sync.WaitGroup,
) {
defer wg.Done()
for {
if ctx.Err() != nil {
return
}
func() {
startTime := time.Now()
response, err := request.Send(ctx, timeout)
completedTime := time.Since(startTime)
if response != nil {
defer fasthttp.ReleaseResponse(response)
}
if err != nil {
if err == types.ErrInterrupt {
return
}
*responseData = append(*responseData, &Response{
Response: err.Error(),
Time: completedTime,
})
increase <- 1
return
}
*responseData = append(*responseData, &Response{
Response: strconv.Itoa(response.StatusCode()),
Time: completedTime, Time: completedTime,
}) })
increase <- 1 increase <- 1

94
types/body.go Normal file
View File

@ -0,0 +1,94 @@
package types
import (
"bytes"
"encoding/json"
"fmt"
"github.com/jedib0t/go-pretty/v6/text"
)
type Body []string
func (body Body) String() string {
var buffer bytes.Buffer
if len(body) == 0 {
return buffer.String()
}
if len(body) == 1 {
buffer.WriteString(body[0])
return buffer.String()
}
buffer.WriteString(text.FgBlue.Sprint("Random") + "[\n")
indent := " "
displayLimit := 5
for i, item := range body[:min(len(body), displayLimit)] {
if i > 0 {
buffer.WriteString(",\n")
}
buffer.WriteString(indent + item)
}
// Add remaining count if there are more items
if remainingValues := len(body) - displayLimit; remainingValues > 0 {
buffer.WriteString(",\n" + indent + text.FgGreen.Sprintf("+%d bodies", remainingValues))
}
buffer.WriteString("\n]")
return buffer.String()
}
func (body *Body) UnmarshalJSON(b []byte) error {
var data any
if err := json.Unmarshal(b, &data); err != nil {
return err
}
switch v := data.(type) {
case string:
*body = []string{v}
case []any:
var slice []string
for _, item := range v {
slice = append(slice, fmt.Sprintf("%v", item))
}
*body = slice
default:
return fmt.Errorf("invalid type for Body: %T (should be string or []string)", v)
}
return nil
}
func (body *Body) UnmarshalYAML(unmarshal func(interface{}) error) error {
var data any
if err := unmarshal(&data); err != nil {
return err
}
switch v := data.(type) {
case string:
*body = []string{v}
case []any:
var slice []string
for _, item := range v {
slice = append(slice, fmt.Sprintf("%v", item))
}
*body = slice
default:
return fmt.Errorf("invalid type for Body: %T (should be string or []string)", v)
}
return nil
}
func (body *Body) Set(value string) error {
*body = append(*body, value)
return nil
}

32
types/config_file.go Normal file
View File

@ -0,0 +1,32 @@
package types
import "strings"
type FileLocationType int
const (
FileLocationTypeLocal FileLocationType = iota
FileLocationTypeRemoteHTTP
)
type ConfigFile string
func (configFile ConfigFile) String() string {
return string(configFile)
}
func (configFile ConfigFile) LocationType() FileLocationType {
if strings.HasPrefix(string(configFile), "http://") || strings.HasPrefix(string(configFile), "https://") {
return FileLocationTypeRemoteHTTP
}
return FileLocationTypeLocal
}
func (configFile ConfigFile) Extension() string {
i := strings.LastIndex(configFile.String(), ".")
if i == -1 {
return ""
}
return configFile.String()[i+1:]
}

139
types/cookies.go Normal file
View File

@ -0,0 +1,139 @@
package types
import (
"bytes"
"encoding/json"
"fmt"
"strings"
"github.com/jedib0t/go-pretty/v6/text"
)
type Cookies []KeyValue[string, []string]
func (cookies Cookies) String() string {
var buffer bytes.Buffer
if len(cookies) == 0 {
return buffer.String()
}
indent := " "
displayLimit := 3
for i, item := range cookies[:min(len(cookies), displayLimit)] {
if i > 0 {
buffer.WriteString(",\n")
}
if len(item.Value) == 1 {
buffer.WriteString(item.Key + ": " + item.Value[0])
continue
}
buffer.WriteString(item.Key + ": " + text.FgBlue.Sprint("Random") + "[\n")
for ii, v := range item.Value[:min(len(item.Value), displayLimit)] {
if ii == len(item.Value)-1 {
buffer.WriteString(indent + v + "\n")
} else {
buffer.WriteString(indent + v + ",\n")
}
}
// Add remaining values count if needed
if remainingValues := len(item.Value) - displayLimit; remainingValues > 0 {
buffer.WriteString(indent + text.FgGreen.Sprintf("+%d values", remainingValues) + "\n")
}
buffer.WriteString("]")
}
// Add remaining key-value pairs count if needed
if remainingPairs := len(cookies) - displayLimit; remainingPairs > 0 {
buffer.WriteString(",\n" + text.FgGreen.Sprintf("+%d cookies", remainingPairs))
}
return buffer.String()
}
func (cookies *Cookies) AppendByKey(key, value string) {
if item := cookies.GetValue(key); item != nil {
*item = append(*item, value)
} else {
*cookies = append(*cookies, KeyValue[string, []string]{Key: key, Value: []string{value}})
}
}
func (cookies Cookies) GetValue(key string) *[]string {
for i := range cookies {
if cookies[i].Key == key {
return &cookies[i].Value
}
}
return nil
}
func (cookies *Cookies) UnmarshalJSON(b []byte) error {
var data []map[string]any
if err := json.Unmarshal(b, &data); err != nil {
return err
}
for _, item := range data {
for key, value := range item {
switch parsedValue := value.(type) {
case string:
*cookies = append(*cookies, KeyValue[string, []string]{Key: key, Value: []string{parsedValue}})
case []any:
parsedStr := make([]string, len(parsedValue))
for i, item := range parsedValue {
parsedStr[i] = fmt.Sprintf("%v", item)
}
*cookies = append(*cookies, KeyValue[string, []string]{Key: key, Value: parsedStr})
default:
return fmt.Errorf("unsupported type for cookies expected string or []string, got %T", parsedValue)
}
}
}
return nil
}
func (cookies *Cookies) UnmarshalYAML(unmarshal func(interface{}) error) error {
var raw []map[string]any
if err := unmarshal(&raw); err != nil {
return err
}
for _, param := range raw {
for key, value := range param {
switch parsed := value.(type) {
case string:
*cookies = append(*cookies, KeyValue[string, []string]{Key: key, Value: []string{parsed}})
case []any:
var values []string
for _, v := range parsed {
if str, ok := v.(string); ok {
values = append(values, str)
}
}
*cookies = append(*cookies, KeyValue[string, []string]{Key: key, Value: values})
}
}
}
return nil
}
func (cookies *Cookies) Set(value string) error {
parts := strings.SplitN(value, "=", 2)
switch len(parts) {
case 0:
cookies.AppendByKey("", "")
case 1:
cookies.AppendByKey(parts[0], "")
case 2:
cookies.AppendByKey(parts[0], parts[1])
}
return nil
}

57
types/duration.go Normal file
View File

@ -0,0 +1,57 @@
package types
import (
"encoding/json"
"errors"
"time"
)
type Duration struct {
time.Duration
}
func (duration *Duration) UnmarshalJSON(b []byte) error {
var v any
if err := json.Unmarshal(b, &v); err != nil {
return err
}
switch value := v.(type) {
case float64:
duration.Duration = time.Duration(value)
return nil
case string:
var err error
duration.Duration, err = time.ParseDuration(value)
if err != nil {
return errors.New("Duration is invalid (e.g. 400ms, 1s, 5m, 1h)")
}
return nil
default:
return errors.New("Duration is invalid (e.g. 400ms, 1s, 5m, 1h)")
}
}
func (duration Duration) MarshalJSON() ([]byte, error) {
return json.Marshal(duration.String())
}
func (duration *Duration) UnmarshalYAML(unmarshal func(interface{}) error) error {
var v any
if err := unmarshal(&v); err != nil {
return err
}
switch value := v.(type) {
case float64:
duration.Duration = time.Duration(value)
return nil
case string:
var err error
duration.Duration, err = time.ParseDuration(value)
if err != nil {
return errors.New("Duration is invalid (e.g. 400ms, 1s, 5m, 1h)")
}
return nil
default:
return errors.New("Duration is invalid (e.g. 400ms, 1s, 5m, 1h)")
}
}

41
types/durations.go Normal file
View File

@ -0,0 +1,41 @@
package types
import (
"sort"
"time"
)
type Durations []time.Duration
func (d Durations) Sort(ascending ...bool) {
// If ascending is provided and is false, sort in descending order
if len(ascending) > 0 && ascending[0] == false {
sort.Slice(d, func(i, j int) bool {
return d[i] > d[j]
})
} else { // Otherwise, sort in ascending order
sort.Slice(d, func(i, j int) bool {
return d[i] < d[j]
})
}
}
func (d Durations) First() *time.Duration {
return &d[0]
}
func (d Durations) Last() *time.Duration {
return &d[len(d)-1]
}
func (d Durations) Sum() time.Duration {
sum := time.Duration(0)
for _, duration := range d {
sum += duration
}
return sum
}
func (d Durations) Avg() time.Duration {
return d.Sum() / time.Duration(len(d))
}

10
types/errors.go Normal file
View File

@ -0,0 +1,10 @@
package types
import (
"errors"
)
var (
ErrInterrupt = errors.New("interrupted")
ErrTimeout = errors.New("timeout")
)

156
types/headers.go Normal file
View File

@ -0,0 +1,156 @@
package types
import (
"bytes"
"encoding/json"
"fmt"
"strings"
"github.com/jedib0t/go-pretty/v6/text"
)
type Headers []KeyValue[string, []string]
func (headers Headers) String() string {
var buffer bytes.Buffer
if len(headers) == 0 {
return buffer.String()
}
indent := " "
displayLimit := 3
for i, item := range headers[:min(len(headers), displayLimit)] {
if i > 0 {
buffer.WriteString(",\n")
}
if len(item.Value) == 1 {
buffer.WriteString(item.Key + ": " + item.Value[0])
continue
}
buffer.WriteString(item.Key + ": " + text.FgBlue.Sprint("Random") + "[\n")
for ii, v := range item.Value[:min(len(item.Value), displayLimit)] {
if ii == len(item.Value)-1 {
buffer.WriteString(indent + v + "\n")
} else {
buffer.WriteString(indent + v + ",\n")
}
}
// Add remaining values count if needed
if remainingValues := len(item.Value) - displayLimit; remainingValues > 0 {
buffer.WriteString(indent + text.FgGreen.Sprintf("+%d values", remainingValues) + "\n")
}
buffer.WriteString("]")
}
// Add remaining key-value pairs count if needed
if remainingPairs := len(headers) - displayLimit; remainingPairs > 0 {
buffer.WriteString(",\n" + text.FgGreen.Sprintf("+%d headers", remainingPairs))
}
return buffer.String()
}
func (headers *Headers) AppendByKey(key, value string) {
if item := headers.GetValue(key); item != nil {
*item = append(*item, value)
} else {
*headers = append(*headers, KeyValue[string, []string]{Key: key, Value: []string{value}})
}
}
func (headers Headers) GetValue(key string) *[]string {
for i := range headers {
if headers[i].Key == key {
return &headers[i].Value
}
}
return nil
}
func (headers Headers) Has(key string) bool {
for i := range headers {
if headers[i].Key == key {
return true
}
}
return false
}
func (headers *Headers) UnmarshalJSON(b []byte) error {
var data []map[string]any
if err := json.Unmarshal(b, &data); err != nil {
return err
}
for _, item := range data {
for key, value := range item {
switch parsedValue := value.(type) {
case string:
*headers = append(*headers, KeyValue[string, []string]{Key: key, Value: []string{parsedValue}})
case []any:
parsedStr := make([]string, len(parsedValue))
for i, item := range parsedValue {
parsedStr[i] = fmt.Sprintf("%v", item)
}
*headers = append(*headers, KeyValue[string, []string]{Key: key, Value: parsedStr})
default:
return fmt.Errorf("unsupported type for headers expected string or []string, got %T", parsedValue)
}
}
}
return nil
}
func (headers *Headers) UnmarshalYAML(unmarshal func(interface{}) error) error {
var raw []map[string]any
if err := unmarshal(&raw); err != nil {
return err
}
for _, param := range raw {
for key, value := range param {
switch parsed := value.(type) {
case string:
*headers = append(*headers, KeyValue[string, []string]{Key: key, Value: []string{parsed}})
case []any:
var values []string
for _, v := range parsed {
if str, ok := v.(string); ok {
values = append(values, str)
}
}
*headers = append(*headers, KeyValue[string, []string]{Key: key, Value: values})
}
}
}
return nil
}
func (headers *Headers) Set(value string) error {
parts := strings.SplitN(value, ":", 2)
switch len(parts) {
case 0:
headers.AppendByKey("", "")
case 1:
headers.AppendByKey(parts[0], "")
case 2:
headers.AppendByKey(parts[0], parts[1])
}
return nil
}
func (headers *Headers) SetIfNotExists(key string, value string) bool {
if headers.Has(key) {
return false
}
*headers = append(*headers, KeyValue[string, []string]{Key: key, Value: []string{value}})
return true
}

6
types/key_value.go Normal file
View File

@ -0,0 +1,6 @@
package types
type KeyValue[K comparable, V any] struct {
Key K
Value V
}

139
types/params.go Normal file
View File

@ -0,0 +1,139 @@
package types
import (
"bytes"
"encoding/json"
"fmt"
"strings"
"github.com/jedib0t/go-pretty/v6/text"
)
type Params []KeyValue[string, []string]
func (params Params) String() string {
var buffer bytes.Buffer
if len(params) == 0 {
return buffer.String()
}
indent := " "
displayLimit := 3
for i, item := range params[:min(len(params), displayLimit)] {
if i > 0 {
buffer.WriteString(",\n")
}
if len(item.Value) == 1 {
buffer.WriteString(item.Key + ": " + item.Value[0])
continue
}
buffer.WriteString(item.Key + ": " + text.FgBlue.Sprint("Random") + "[\n")
for ii, v := range item.Value[:min(len(item.Value), displayLimit)] {
if ii == len(item.Value)-1 {
buffer.WriteString(indent + v + "\n")
} else {
buffer.WriteString(indent + v + ",\n")
}
}
// Add remaining values count if needed
if remainingValues := len(item.Value) - displayLimit; remainingValues > 0 {
buffer.WriteString(indent + text.FgGreen.Sprintf("+%d values", remainingValues) + "\n")
}
buffer.WriteString("]")
}
// Add remaining key-value pairs count if needed
if remainingPairs := len(params) - displayLimit; remainingPairs > 0 {
buffer.WriteString(",\n" + text.FgGreen.Sprintf("+%d params", remainingPairs))
}
return buffer.String()
}
func (params *Params) AppendByKey(key, value string) {
if item := params.GetValue(key); item != nil {
*item = append(*item, value)
} else {
*params = append(*params, KeyValue[string, []string]{Key: key, Value: []string{value}})
}
}
func (params Params) GetValue(key string) *[]string {
for i := range params {
if params[i].Key == key {
return &params[i].Value
}
}
return nil
}
func (params *Params) UnmarshalJSON(b []byte) error {
var data []map[string]any
if err := json.Unmarshal(b, &data); err != nil {
return err
}
for _, item := range data {
for key, value := range item {
switch parsedValue := value.(type) {
case string:
*params = append(*params, KeyValue[string, []string]{Key: key, Value: []string{parsedValue}})
case []any:
parsedStr := make([]string, len(parsedValue))
for i, item := range parsedValue {
parsedStr[i] = fmt.Sprintf("%v", item)
}
*params = append(*params, KeyValue[string, []string]{Key: key, Value: parsedStr})
default:
return fmt.Errorf("unsupported type for params expected string or []string, got %T", parsedValue)
}
}
}
return nil
}
func (params *Params) UnmarshalYAML(unmarshal func(interface{}) error) error {
var raw []map[string]any
if err := unmarshal(&raw); err != nil {
return err
}
for _, param := range raw {
for key, value := range param {
switch parsed := value.(type) {
case string:
*params = append(*params, KeyValue[string, []string]{Key: key, Value: []string{parsed}})
case []any:
var values []string
for _, v := range parsed {
if str, ok := v.(string); ok {
values = append(values, str)
}
}
*params = append(*params, KeyValue[string, []string]{Key: key, Value: values})
}
}
}
return nil
}
func (params *Params) Set(value string) error {
parts := strings.SplitN(value, "=", 2)
switch len(parts) {
case 0:
params.AppendByKey("", "")
case 1:
params.AppendByKey(parts[0], "")
case 2:
params.AppendByKey(parts[0], parts[1])
}
return nil
}

116
types/proxies.go Normal file
View File

@ -0,0 +1,116 @@
package types
import (
"bytes"
"encoding/json"
"fmt"
"net/url"
"github.com/jedib0t/go-pretty/v6/text"
)
type Proxies []url.URL
func (proxies Proxies) String() string {
var buffer bytes.Buffer
if len(proxies) == 0 {
return buffer.String()
}
if len(proxies) == 1 {
buffer.WriteString(proxies[0].String())
return buffer.String()
}
buffer.WriteString(text.FgBlue.Sprint("Random") + "[\n")
indent := " "
displayLimit := 5
for i, item := range proxies[:min(len(proxies), displayLimit)] {
if i > 0 {
buffer.WriteString(",\n")
}
buffer.WriteString(indent + item.String())
}
// Add remaining count if there are more items
if remainingValues := len(proxies) - displayLimit; remainingValues > 0 {
buffer.WriteString(",\n" + indent + text.FgGreen.Sprintf("+%d proxies", remainingValues))
}
buffer.WriteString("\n]")
return buffer.String()
}
func (proxies *Proxies) UnmarshalJSON(b []byte) error {
var data any
if err := json.Unmarshal(b, &data); err != nil {
return err
}
switch v := data.(type) {
case string:
parsed, err := url.Parse(v)
if err != nil {
return err
}
*proxies = []url.URL{*parsed}
case []any:
var urls []url.URL
for _, item := range v {
url, err := url.Parse(item.(string))
if err != nil {
return err
}
urls = append(urls, *url)
}
*proxies = urls
default:
return fmt.Errorf("invalid type for Body: %T (should be URL or []URL)", v)
}
return nil
}
func (proxies *Proxies) UnmarshalYAML(unmarshal func(interface{}) error) error {
var data any
if err := unmarshal(&data); err != nil {
return err
}
switch v := data.(type) {
case string:
parsed, err := url.Parse(v)
if err != nil {
return err
}
*proxies = []url.URL{*parsed}
case []any:
var urls []url.URL
for _, item := range v {
url, err := url.Parse(item.(string))
if err != nil {
return err
}
urls = append(urls, *url)
}
*proxies = urls
default:
return fmt.Errorf("invalid type for Body: %T (should be URL or []URL)", v)
}
return nil
}
func (proxies *Proxies) Set(value string) error {
parsedURL, err := url.Parse(value)
if err != nil {
return err
}
*proxies = append(*proxies, *parsedURL)
return nil
}

59
types/request_url.go Normal file
View File

@ -0,0 +1,59 @@
package types
import (
"encoding/json"
"errors"
"net/url"
)
type RequestURL struct {
url.URL
}
func (requestURL *RequestURL) UnmarshalJSON(data []byte) error {
var urlStr string
if err := json.Unmarshal(data, &urlStr); err != nil {
return err
}
parsedURL, err := url.Parse(urlStr)
if err != nil {
return errors.New("request URL is invalid")
}
requestURL.URL = *parsedURL
return nil
}
func (requestURL *RequestURL) UnmarshalYAML(unmarshal func(interface{}) error) error {
var urlStr string
if err := unmarshal(&urlStr); err != nil {
return err
}
parsedURL, err := url.Parse(urlStr)
if err != nil {
return errors.New("request URL is invalid")
}
requestURL.URL = *parsedURL
return nil
}
func (requestURL RequestURL) MarshalJSON() ([]byte, error) {
return json.Marshal(requestURL.URL.String())
}
func (requestURL RequestURL) String() string {
return requestURL.URL.String()
}
func (requestURL *RequestURL) Set(value string) error {
parsedURL, err := url.Parse(value)
if err != nil {
return err
}
requestURL.URL = *parsedURL
return nil
}

57
types/timeout.go Normal file
View File

@ -0,0 +1,57 @@
package types
import (
"encoding/json"
"errors"
"time"
)
type Timeout struct {
time.Duration
}
func (timeout *Timeout) UnmarshalJSON(b []byte) error {
var v any
if err := json.Unmarshal(b, &v); err != nil {
return err
}
switch value := v.(type) {
case float64:
timeout.Duration = time.Duration(value)
return nil
case string:
var err error
timeout.Duration, err = time.ParseDuration(value)
if err != nil {
return errors.New("Timeout is invalid (e.g. 400ms, 1s, 5m, 1h)")
}
return nil
default:
return errors.New("Timeout is invalid (e.g. 400ms, 1s, 5m, 1h)")
}
}
func (timeout Timeout) MarshalJSON() ([]byte, error) {
return json.Marshal(timeout.String())
}
func (timeout *Timeout) UnmarshalYAML(unmarshal func(interface{}) error) error {
var v any
if err := unmarshal(&v); err != nil {
return err
}
switch value := v.(type) {
case float64:
timeout.Duration = time.Duration(value)
return nil
case string:
var err error
timeout.Duration, err = time.ParseDuration(value)
if err != nil {
return errors.New("Timeout is invalid (e.g. 400ms, 1s, 5m, 1h)")
}
return nil
default:
return errors.New("Timeout is invalid (e.g. 400ms, 1s, 5m, 1h)")
}
}

10
utils/compare.go Normal file
View File

@ -0,0 +1,10 @@
package utils
func IsNilOrZero[T comparable](value *T) bool {
if value == nil {
return true
}
var zero T
return *value == zero
}

View File

@ -1,54 +1,5 @@
package utils package utils
import ( func ToPtr[T any](value T) *T {
"encoding/json" return &value
"fmt"
"reflect"
)
func MarshalJSON(v any, maxSliceSize uint) string {
rv := reflect.ValueOf(v)
if rv.Kind() == reflect.Slice && rv.Len() == 0 {
return "[]"
}
data, err := json.MarshalIndent(truncateLists(v, int(maxSliceSize)), "", " ")
if err != nil {
return "{}"
}
return string(data)
}
func truncateLists(v interface{}, maxItems int) interface{} {
rv := reflect.ValueOf(v)
switch rv.Kind() {
case reflect.Slice, reflect.Array:
if rv.Len() > maxItems {
newSlice := reflect.MakeSlice(rv.Type(), maxItems, maxItems)
reflect.Copy(newSlice, rv.Slice(0, maxItems))
newSlice = reflect.Append(newSlice, reflect.ValueOf(fmt.Sprintf("...(%d more)", rv.Len()-maxItems)))
return newSlice.Interface()
}
case reflect.Map:
newMap := reflect.MakeMap(rv.Type())
for _, key := range rv.MapKeys() {
newMap.SetMapIndex(key, reflect.ValueOf(truncateLists(rv.MapIndex(key).Interface(), maxItems)))
}
return newMap.Interface()
case reflect.Struct:
newStruct := reflect.New(rv.Type()).Elem()
for i := 0; i < rv.NumField(); i++ {
newStruct.Field(i).Set(reflect.ValueOf(truncateLists(rv.Field(i).Interface(), maxItems)))
}
return newStruct.Interface()
case reflect.Ptr:
if rv.IsNil() {
return nil
}
return truncateLists(rv.Elem().Interface(), maxItems)
}
return v
} }

21
utils/int.go Normal file
View File

@ -0,0 +1,21 @@
package utils
type Number interface {
int | int8 | int16 | int32 | int64
}
func NumLen[T Number](n T) T {
if n < 0 {
n = -n
}
if n == 0 {
return 1
}
var count T = 0
for n > 0 {
n /= 10
count++
}
return count
}

View File

@ -3,46 +3,12 @@ package utils
import ( import (
"fmt" "fmt"
"os" "os"
"github.com/jedib0t/go-pretty/v6/text"
) )
var Colors = struct {
reset string
Red string
Green string
Yellow string
Orange string
Blue string
Magenta string
Cyan string
Gray string
White string
}{
reset: "\033[0m",
Red: "\033[31m",
Green: "\033[32m",
Yellow: "\033[33m",
Orange: "\033[38;5;208m",
Blue: "\033[34m",
Magenta: "\033[35m",
Cyan: "\033[36m",
Gray: "\033[37m",
White: "\033[97m",
}
func Colored(color string, a ...any) string {
return color + fmt.Sprint(a...) + Colors.reset
}
func PrintfC(color string, format string, a ...any) {
fmt.Printf(Colored(color, format), a...)
}
func PrintlnC(color string, a ...any) {
fmt.Println(Colored(color, a...))
}
func PrintErr(err error) { func PrintErr(err error) {
PrintlnC(Colors.Red, err.Error()) fmt.Fprintln(os.Stderr, text.FgRed.Sprint(err.Error()))
} }
func PrintErrAndExit(err error) { func PrintErrAndExit(err error) {

View File

@ -10,15 +10,6 @@ func Flatten[T any](nested [][]*T) []*T {
return flattened return flattened
} }
func Contains[T comparable](slice []T, item T) bool {
for _, i := range slice {
if i == item {
return true
}
}
return false
}
// RandomValueCycle returns a function that cycles through the provided slice of values // RandomValueCycle returns a function that cycles through the provided slice of values
// in a random order. Each call to the returned function will yield a value from the slice. // in a random order. Each call to the returned function will yield a value from the slice.
// The order of values is determined by the provided random number generator. // The order of values is determined by the provided random number generator.
@ -29,9 +20,9 @@ func Contains[T comparable](slice []T, item T) bool {
// The returned function isn't thread-safe and should be used in a single-threaded context. // The returned function isn't thread-safe and should be used in a single-threaded context.
func RandomValueCycle[Value any](values []Value, localRand *rand.Rand) func() Value { func RandomValueCycle[Value any](values []Value, localRand *rand.Rand) func() Value {
var ( var (
clientsCount int = len(values) clientsCount = len(values)
currentIndex int = localRand.Intn(clientsCount) currentIndex = localRand.Intn(clientsCount)
stopIndex int = currentIndex stopIndex = currentIndex
) )
return func() Value { return func() Value {

View File

@ -2,30 +2,13 @@ package utils
import "time" import "time"
func MinDuration(durations ...time.Duration) time.Duration { func DurationRoundBy(duration time.Duration, n int64) time.Duration {
min := durations[0] if durationLen := NumLen(duration.Nanoseconds()); durationLen > n {
for _, d := range durations { roundNum := 1
if d < min { for range durationLen - n {
min = d roundNum *= 10
} }
return duration.Round(time.Duration(roundNum))
} }
return min return duration
}
func MaxDuration(durations ...time.Duration) time.Duration {
max := durations[0]
for _, d := range durations {
if d > max {
max = d
}
}
return max
}
func AvgDuration(durations ...time.Duration) time.Duration {
total := time.Duration(0)
for _, d := range durations {
total += d
}
return total / time.Duration(len(durations))
} }

View File

@ -1,87 +0,0 @@
package utils
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
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}
}

View File

@ -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
}