130 Commits

Author SHA1 Message Date
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
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
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
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
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
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
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
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
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
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
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
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
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
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
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
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
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
fa55de0cf2 Merge pull request #39 from aykhans/feat/add-proxy-check
Add 'no-proxy-check' parameter
2024-11-28 04:22:12 +04:00
d392d4a787 🔨 Refactor method 'UnmarshalJSON' of 'option[T]' 2024-11-28 04:20:01 +04:00
2e6110e54a 🔥 Remove assets 2024-11-27 01:30:15 +04:00
48721b4ae7 Update README.md 2024-11-27 01:28:07 +04:00
fe4a93b394 🔨 Add constructors to config structs 2024-11-27 00:46:14 +04:00
0725c1a481 🔨 Refactor 'option[T]' type 2024-11-24 16:54:38 +04:00
8d983b4313 🔨 Rename 'IOption' to 'Option' 2024-11-24 02:43:22 +04:00
e3cbe1e9b4 Merge pull request #42 from aykhans/bump-go-version
🔖 Bump go version '1.22.6 -> 1.23.2'
2024-11-23 18:02:00 +04:00
94211eb018 🔖 Bump go version '1.22.6 -> 1.23.2' 2024-11-23 18:01:03 +04:00
565079fe97 Merge pull request #41 from aykhans/add-workflow-1
Rename publish-docker-image to publish-docker-image.yml
2024-11-23 01:19:56 +04:00
696176e611 Rename publish-docker-image to publish-docker-image.yml 2024-11-23 01:19:38 +04:00
9ea06593f9 Merge pull request #40 from aykhans/add-workflow
Create publish-docker-image
2024-11-23 01:18:09 +04:00
7f7dc07f4c Create publish-docker-image 2024-11-23 01:17:24 +04:00
45 changed files with 2255 additions and 1450 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@v6
with:
version: v1.64
args: --timeout=10m --config=.golangci.yml

View File

@ -0,0 +1,86 @@
name: publish-docker-image
on:
push:
tags:
# Match stable and pre versions, such as 'v1.0.0', 'v0.23.0-a', 'v0.23.0-a.2', 'v0.23.0-b', 'v0.23.0-b.3'
- "v*.*.*"
- "v*.*.*-a"
- "v*.*.*-a.*"
- "v*.*.*-b"
- "v*.*.*-b.*"
jobs:
build-and-push-stable-image:
runs-on: ubuntu-latest
permissions:
contents: read
packages: write
steps:
- uses: actions/checkout@v4
- name: Set up QEMU
uses: docker/setup-qemu-action@v3
- name: Extract build args
# Extract version number and check if it's an pre version
run: |
if [[ "${GITHUB_REF_NAME}" =~ ^v[0-9]+\.[0-9]+\.[0-9]+$ ]]; then
echo "PRE_RELEASE=false" >> $GITHUB_ENV
else
echo "PRE_RELEASE=true" >> $GITHUB_ENV
fi
echo "VERSION=${GITHUB_REF_NAME#v}" >> $GITHUB_ENV
- name: Login to Docker Hub
uses: docker/login-action@v3
with:
username: aykhans
password: ${{ secrets.DOCKER_TOKEN }}
- name: Set up Docker Buildx
id: buildx
uses: docker/setup-buildx-action@v3
with:
install: true
version: v0.9.1
# Metadata for stable versions
- name: Docker meta for stable
id: meta-stable
if: env.PRE_RELEASE == 'false'
uses: docker/metadata-action@v5
with:
images: |
aykhans/dodo
tags: |
type=semver,pattern={{version}},value=${{ env.VERSION }}
type=raw,value=stable
flavor: |
latest=true
labels: |
org.opencontainers.image.version=${{ env.VERSION }}
# Metadata for pre versions
- name: Docker meta for pre
id: meta-pre
if: env.PRE_RELEASE == 'true'
uses: docker/metadata-action@v5
with:
images: |
aykhans/dodo
tags: |
type=raw,value=${{ env.VERSION }}
labels: |
org.opencontainers.image.version=${{ env.VERSION }}
- name: Build and Push
id: docker_build
uses: docker/build-push-action@v6
with:
context: ./
file: ./Dockerfile
platforms: linux/amd64,linux/arm64
push: true
tags: ${{ steps.meta-stable.outputs.tags || steps.meta-pre.outputs.tags }}
labels: ${{ steps.meta-stable.outputs.labels || steps.meta-pre.outputs.labels }}

27
.golangci.yml Normal file
View File

@ -0,0 +1,27 @@
run:
go: "1.24"
concurrency: 8
timeout: 10m
linters:
disable-all: true
enable:
- asasalint
- asciicheck
- errcheck
- gofmt
- goimports
- gomodguard
- goprintffuncname
- govet
- ineffassign
- misspell
- nakedret
- nolintlint
- prealloc
- reassign
- staticcheck
- typecheck
- unconvert
- unused
- whitespace

View File

@ -1,19 +1,17 @@
FROM golang:1.22.6-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"]

9
Makefile Normal file
View File

@ -0,0 +1,9 @@
lint:
golangci-lint run
build:
go build -ldflags "-s -w" -o "./dodo"
build-all:
rm -rf ./binaries
./build.sh

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

Binary file not shown.

Before

Width:  |  Height:  |  Size: 230 KiB

View File

@ -1,22 +1,36 @@
{ {
"method": "GET", "method": "GET",
"url": "https://example.com", "url": "https://example.com",
"no_proxy_check": false, "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,159 +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.2"
DefaultUserAgent string = "Dodo/" + VERSION DefaultUserAgent string = "Dodo/" + VERSION
ProxyCheckURL string = "https://www.google.com" DefaultMethod string = "GET"
DefaultMethod string = "GET" DefaultTimeout time.Duration = time.Second * 10
DefaultTimeout uint32 = 10000 // Milliseconds (10 seconds) DefaultDodosCount uint = 1
DefaultDodosCount uint = 1 DefaultRequestCount uint = 0
DefaultRequestCount uint = 1000 DefaultDuration time.Duration = 0
MaxDodosCountForProxies uint = 20 // Max dodos count for proxy check DefaultYes bool = false
) )
type IConfig interface { var SupportedProxySchemes []string = []string{"http", "socks5", "socks5h"}
MergeConfigs(newConfig IConfig) IConfig
}
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,
t.AppendHeader(table.Row{"Request Configuration"}) Yes: *conf.Yes,
t.AppendRow(table.Row{"Method", config.Method}) Params: conf.Params,
t.AppendSeparator() Headers: conf.Headers,
t.AppendRow(table.Row{"URL", config.URL}) Cookies: conf.Cookies,
t.AppendSeparator() Body: conf.Body,
t.AppendRow(table.Row{"Timeout", fmt.Sprintf("%dms", config.Timeout/time.Millisecond)}) Proxies: conf.Proxies,
t.AppendSeparator() }
t.AppendRow(table.Row{"Dodos", config.DodosCount})
t.AppendSeparator()
t.AppendRow(table.Row{"Request", config.RequestCount})
t.AppendSeparator()
t.AppendRow(table.Row{"Params", utils.MarshalJSON(config.Params, 3)})
t.AppendSeparator()
t.AppendRow(table.Row{"Headers", utils.MarshalJSON(config.Headers, 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)
} }
type Config struct { func (rc *RequestConfig) Print() {
Method string `json:"method" validate:"http_method"` // custom validations: http_method t := table.NewWriter()
URL string `json:"url" validate:"http_url,required"` t.SetOutputMirror(os.Stdout)
Timeout uint32 `json:"timeout" validate:"gte=1,lte=100000"` t.SetStyle(table.StyleLight)
DodosCount uint `json:"dodos_count" validate:"gte=1"` t.SetColumnConfigs([]table.ColumnConfig{
RequestCount uint `json:"request_count" validation_name:"request-count" validate:"gte=1"` {
NoProxyCheck utils.IOption[bool] `json:"no_proxy_check"` 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()
} }
func (config *Config) MergeConfigs(newConfig *Config) { type Config struct {
if newConfig.Method != "" { Method *string `json:"method" yaml:"method"`
URL *types.RequestURL `json:"url" yaml:"url"`
Timeout *types.Timeout `json:"timeout" yaml:"timeout"`
DodosCount *uint `json:"dodos" yaml:"dodos"`
RequestCount *uint `json:"requests" yaml:"requests"`
Duration *types.Duration `json:"duration" yaml:"duration"`
Yes *bool `json:"yes" yaml:"yes"`
Params types.Params `json:"params" yaml:"params"`
Headers types.Headers `json:"headers" yaml:"headers"`
Cookies types.Cookies `json:"cookies" yaml:"cookies"`
Body types.Body `json:"body" yaml:"body"`
Proxies types.Proxies `json:"proxy" yaml:"proxy"`
}
func NewConfig() *Config {
return &Config{}
}
func (c *Config) Validate() []error {
var errs []error
if utils.IsNilOrZero(c.URL) {
errs = append(errs, errors.New("request URL is required"))
} else {
if c.URL.Scheme == "" {
c.URL.Scheme = "http"
}
if c.URL.Scheme != "http" && c.URL.Scheme != "https" {
errs = append(errs, errors.New("request URL scheme must be http or https"))
}
urlParams := types.Params{}
for key, values := range c.URL.Query() {
for _, value := range values {
urlParams = append(urlParams, types.KeyValue[string, []string]{
Key: key,
Value: []string{value},
})
}
}
c.Params = append(urlParams, c.Params...)
c.URL.RawQuery = ""
}
if utils.IsNilOrZero(c.Method) {
errs = append(errs, errors.New("request method is required"))
}
if utils.IsNilOrZero(c.Timeout) {
errs = append(errs, errors.New("request timeout must be greater than 0"))
}
if utils.IsNilOrZero(c.DodosCount) {
errs = append(errs, errors.New("dodos count must be greater than 0"))
}
if utils.IsNilOrZero(c.Duration) && utils.IsNilOrZero(c.RequestCount) {
errs = append(errs, errors.New("you should provide at least one of duration or request count"))
}
for i, proxy := range c.Proxies {
if proxy.String() == "" {
errs = append(errs, fmt.Errorf("proxies[%d]: proxy cannot be empty", i))
} else if schema := proxy.Scheme; !slices.Contains(SupportedProxySchemes, schema) {
errs = append(errs,
fmt.Errorf("proxies[%d]: proxy has unsupported scheme \"%s\" (supported schemes: %s)",
i, proxy.String(), strings.Join(SupportedProxySchemes, ", "),
),
)
}
}
return errs
}
func (config *Config) MergeConfig(newConfig *Config) {
if newConfig.Method != nil {
config.Method = newConfig.Method 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 (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
} }
@ -171,15 +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 (config *CLIConfig) MergeConfigs(newConfig *CLIConfig) {
config.Config.MergeConfigs(&newConfig.Config)
if newConfig.ConfigFile != "" {
config.ConfigFile = newConfig.ConfigFile
} }
if config.Timeout == nil {
config.Timeout = &types.Timeout{Duration: DefaultTimeout}
}
if config.DodosCount == nil {
config.DodosCount = utils.ToPtr(DefaultDodosCount)
}
if config.RequestCount == nil {
config.RequestCount = utils.ToPtr(DefaultRequestCount)
}
if config.Duration == nil {
config.Duration = &types.Duration{Duration: DefaultDuration}
}
if config.Yes == nil {
config.Yes = utils.ToPtr(DefaultYes)
}
config.Headers.SetIfNotExists("User-Agent", DefaultUserAgent)
} }

83
config/file.go Normal file
View File

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

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

26
go.mod
View File

@ -1,29 +1,21 @@
module github.com/aykhans/dodo module github.com/aykhans/dodo
go 1.22.6 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.59.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/go-playground/locales v0.14.1 // indirect
github.com/go-playground/universal-translator v0.18.1 // indirect
github.com/inconshreveable/mousetrap v1.1.0 // indirect
github.com/klauspost/compress v1.17.11 // indirect github.com/klauspost/compress v1.17.11 // indirect
github.com/leodido/go-urn v1.4.0 // indirect github.com/mattn/go-runewidth v0.0.16 // indirect
github.com/mattn/go-runewidth v0.0.15 // indirect
github.com/rivo/uniseg v0.4.7 // indirect github.com/rivo/uniseg v0.4.7 // indirect
github.com/valyala/bytebufferpool v1.0.0 // indirect github.com/valyala/bytebufferpool v1.0.0 // indirect
golang.org/x/crypto v0.29.0 // indirect golang.org/x/net v0.36.0 // indirect
golang.org/x/sys v0.27.0 // indirect golang.org/x/sys v0.30.0 // indirect
golang.org/x/term v0.26.0 // indirect golang.org/x/term v0.29.0 // indirect
golang.org/x/text v0.20.0 // indirect golang.org/x/text v0.22.0 // indirect
) )

55
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/go-playground/assert/v2 v2.2.0/go.mod h1:VDjEfimB/XKnb+ZQfWdccd7VUvScMdVu0Titje2rxJ4=
github.com/go-playground/locales v0.14.1 h1:EWaQ/wswjilfKLTECiXz7Rh+3BjFhfDFKv/oXslEjJA=
github.com/go-playground/locales v0.14.1/go.mod h1:hxrqLVvrK65+Rwrd5Fc6F2O76J/NuW9t0sjnWqG1slY=
github.com/go-playground/universal-translator v0.18.1 h1:Bcnm0ZwsGyWbCzImXv+pAJnYK9S473LQFuzCbDbfSFY=
github.com/go-playground/universal-translator v0.18.1/go.mod h1:xekY+UJKNuX9WP91TpwSH2VMlDf28Uj24BCp08ZFTUY=
github.com/go-playground/validator/v10 v10.23.0 h1:/PwmTwZhS0dPkav3cdK9kV1FsAmrL8sThn8IHr/sO+o=
github.com/go-playground/validator/v10 v10.23.0/go.mod h1:dbuPbCMFw/DrkbEynArYaCwl3amGuJotoKCe95atGMM=
github.com/inconshreveable/mousetrap v1.1.0 h1:wN+x4NVGpMsO7ErUn/mUI3vEoE6Jt13X2s0bqwp9tc8=
github.com/inconshreveable/mousetrap v1.1.0/go.mod h1:vpF70FUmC8bwa3OWnCshd2FqLfsEA9PFc4w1p2J65bw=
github.com/jedib0t/go-pretty/v6 v6.6.2 h1:27bLj3nRODzaiA7tPIxy9UVWHoPspFfME9XxgwiiNsM=
github.com/jedib0t/go-pretty/v6 v6.6.2/go.mod h1:zbn98qrYlh95FIhwwsbIip0LYpwSG8SUOScs+v9/t0E=
github.com/klauspost/compress v1.17.11 h1:In6xLpyWOi1+C7tXUUWv2ot1QvBjxevKAaI6IXrJmUc= github.com/klauspost/compress v1.17.11 h1:In6xLpyWOi1+C7tXUUWv2ot1QvBjxevKAaI6IXrJmUc=
github.com/klauspost/compress v1.17.11/go.mod h1:pMDklpSncoRMuLFrf1W9Ss9KT+0rH90U12bZKk7uwG0= github.com/klauspost/compress v1.17.11/go.mod h1:pMDklpSncoRMuLFrf1W9Ss9KT+0rH90U12bZKk7uwG0=
github.com/leodido/go-urn v1.4.0 h1:WT9HwE9SGECu3lg4d/dIA+jxlljEa1/ffXKmRjqdmIQ= github.com/mattn/go-runewidth v0.0.16 h1:E5ScNMtiwvlvB5paMFdw9p4kSQzbXFikJ5SQO6TULQc=
github.com/leodido/go-urn v1.4.0/go.mod h1:bvxc+MVxLKB4z00jd1z+Dvzr47oO32F/QSNjSBOlFxI= github.com/mattn/go-runewidth v0.0.16/go.mod h1:Jdepj2loyihRzMpdS35Xk/zdY8IAYHsh153qUoGf23w=
github.com/mattn/go-runewidth v0.0.15 h1:UNAjwbU9l54TA3KzvqLGxwWjHmMgBUVhBiTjelZgg3U=
github.com/mattn/go-runewidth v0.0.15/go.mod h1:Jdepj2loyihRzMpdS35Xk/zdY8IAYHsh153qUoGf23w=
github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM= github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM=
github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4=
github.com/rivo/uniseg v0.2.0/go.mod h1:J6wj4VEh+S6ZtnVlnTBMWIodfgj8LQOQFoIToxlJtxc= github.com/rivo/uniseg v0.2.0/go.mod h1:J6wj4VEh+S6ZtnVlnTBMWIodfgj8LQOQFoIToxlJtxc=
github.com/rivo/uniseg v0.4.7 h1:WUdvkW8uEhrYfLC4ZzdpI2ztxP1I582+49Oc5Mq64VQ= github.com/rivo/uniseg v0.4.7 h1:WUdvkW8uEhrYfLC4ZzdpI2ztxP1I582+49Oc5Mq64VQ=
github.com/rivo/uniseg v0.4.7/go.mod h1:FN3SvrM+Zdj16jyLfmOkMNblXMcoc8DfTHruCPUcx88= github.com/rivo/uniseg v0.4.7/go.mod h1:FN3SvrM+Zdj16jyLfmOkMNblXMcoc8DfTHruCPUcx88=
github.com/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.59.0 h1:Qu0qYHfXvPk1mSLNqcFtEk6DpxgA26hy6bmydotDpRI=
github.com/valyala/fasthttp v1.57.0/go.mod h1:h6ZBaPRlzpZ6O3H5t2gEk1Qi33+TmLvfwgLLp0t9CpE= github.com/valyala/fasthttp v1.59.0/go.mod h1:GTxNb9Bc6r2a9D0TWNSPwDz78UxnTGBViY3xZNEqyYU=
github.com/xyproto/randomstring v1.0.5 h1:YtlWPoRdgMu3NZtP45drfy1GKoojuR7hmRcnhZqKjWU= github.com/xyproto/randomstring v1.0.5 h1:YtlWPoRdgMu3NZtP45drfy1GKoojuR7hmRcnhZqKjWU=
github.com/xyproto/randomstring v1.0.5/go.mod h1:rgmS5DeNXLivK7YprL0pY+lTuhNQW3iGxZ18UQApw/E= github.com/xyproto/randomstring v1.0.5/go.mod h1:rgmS5DeNXLivK7YprL0pY+lTuhNQW3iGxZ18UQApw/E=
golang.org/x/crypto v0.29.0 h1:L5SG1JTTXupVV3n6sUqMTeWbjAyfPwoda2DLX8J8FrQ= golang.org/x/net v0.36.0 h1:vWF2fRbw4qslQsQzgFqZff+BItCvGFQqKzKIzx1rmoA=
golang.org/x/crypto v0.29.0/go.mod h1:+F4F4N5hv6v38hfeYwTdx20oUvLLc+QfrE9Ax9HtgRg= golang.org/x/net v0.36.0/go.mod h1:bFmbeoIPfrw4sMHNhb4J9f6+tPziuGjq7Jk/38fxi1I=
golang.org/x/net v0.31.0 h1:68CPQngjLL0r2AlUKiSxtQFKvzRVbnzLwMUn5SzcLHo= golang.org/x/sys v0.30.0 h1:QjkSwP/36a20jFYWkSue1YwXzLmsV5Gfq7Eiy72C1uc=
golang.org/x/net v0.31.0/go.mod h1:P4fl1q7dY2hnZFxEk4pPSkDHF+QqjitcnDjUQyMM+pM= golang.org/x/sys v0.30.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA=
golang.org/x/sys v0.27.0 h1:wBqf8DvsY9Y/2P8gAfPDEYNuS30J4lPHJxXSb/nJZ+s= golang.org/x/term v0.29.0 h1:L6pJp37ocefwRRtYPKSWOWzOtWSxVajvz2ldH/xi3iU=
golang.org/x/sys v0.27.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA= golang.org/x/term v0.29.0/go.mod h1:6bl4lRlvVuDgSf3179VpIxBF0o10JUpXWOnI7nErv7s=
golang.org/x/term v0.26.0 h1:WEQa6V3Gja/BhNxg540hBip/kkaYtRg3cxg4oXSw4AU= golang.org/x/text v0.22.0 h1:bofq7m3/HAFvbF51jz3Q9wLg3jkvSPuiZu/pD1XwgtM=
golang.org/x/term v0.26.0/go.mod h1:Si5m1o57C5nBNQo5z1iq+XDijt21BDBDp2bK0QI8e3E= golang.org/x/text v0.22.0/go.mod h1:YRoo4H8PVmsu+E3Ou7cqLVH8oXWIHVoX0jqUWALQhfY=
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=

126
main.go
View File

@ -2,115 +2,73 @@ package main
import ( import (
"context" "context"
"errors"
"fmt" "fmt"
"net/url"
"os" "os"
"os/signal" "os/signal"
"strings"
"syscall" "syscall"
"time" "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.Config{} configFile, err := conf.ReadCLI()
jsonConf := config.JSONConfig{}
cliConf, err := readers.CLIConfigReader()
if err != nil || cliConf == nil {
os.Exit(0)
}
if err := validator.StructPartial(cliConf, "ConfigFile"); err != nil {
utils.PrintErrAndExit(
customerrors.ValidationErrorsFormater(
err.(goValidator.ValidationErrors),
),
)
}
if cliConf.ConfigFile != "" {
jsonConfNew, err := readers.JSONConfigReader(cliConf.ConfigFile)
if err != nil {
utils.PrintErrAndExit(err)
}
if err := validator.StructFiltered(
jsonConfNew,
func(ns []byte) bool {
return strings.LastIndex(string(ns), "Proxies") == -1
}); err != nil {
utils.PrintErrAndExit(
customerrors.ValidationErrorsFormater(
err.(goValidator.ValidationErrors),
),
)
}
jsonConf = *jsonConfNew
conf.MergeConfigs(&jsonConf.Config)
}
conf.MergeConfigs(&cliConf.Config)
conf.SetDefaults()
if err := validator.Struct(conf); err != nil {
utils.PrintErrAndExit(
customerrors.ValidationErrorsFormater(
err.(goValidator.ValidationErrors),
),
)
}
parsedURL, err := url.Parse(conf.URL)
if err != nil { if err != nil {
utils.PrintErrAndExit(err) utils.PrintErrAndExit(err)
} }
requestConf := &config.RequestConfig{
Method: conf.Method, if configFile.String() != "" {
URL: parsedURL, tempConf := config.NewConfig()
Timeout: time.Duration(conf.Timeout) * time.Millisecond, if err := tempConf.ReadFile(configFile); err != nil {
DodosCount: conf.DodosCount, utils.PrintErrAndExit(err)
RequestCount: conf.RequestCount, }
Params: jsonConf.Params, tempConf.MergeConfig(conf)
Headers: jsonConf.Headers, conf = tempConf
Cookies: jsonConf.Cookies, }
Proxies: jsonConf.Proxies, conf.SetDefaults()
Body: jsonConf.Body,
Yes: cliConf.Yes, if errs := conf.Validate(); len(errs) > 0 {
NoProxyCheck: *conf.NoProxyCheck.ValueOrPanic(), utils.PrintErrAndExit(errors.Join(errs...))
} }
requestConf.Print()
if !cliConf.Yes { requestConf := config.NewRequestConfig(conf)
response := readers.CLIYesOrNoReader("Do you want to continue?", true) requestConf.Print()
if !response {
utils.PrintAndExit("Exiting...") if !requestConf.Yes {
response := config.CLIYesOrNoReader("Do you want to continue?", false)
if !response {
utils.PrintAndExit("Exiting...\n")
} }
fmt.Println()
} }
ctx, cancel := context.WithCancel(context.Background()) ctx, cancel := context.WithCancel(context.Background())
sigChan := make(chan os.Signal, 1) go listenForTermination(func() { cancel() })
signal.Notify(sigChan, syscall.SIGINT, syscall.SIGTERM)
go func() { if requestConf.Duration > 0 {
<-sigChan time.AfterFunc(requestConf.Duration, func() { cancel() })
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,100 +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.CLIConfig{
Config: config.Config{
NoProxyCheck: utils.NewNoneOption[bool](),
},
}
dodosCount uint
requestCount uint
timeout uint32
noProxyCheck bool
rootCmd = &cobra.Command{
Use: "dodo [flags]",
Example: ` Simple usage only with URL:
dodo -u https://example.com
Simple usage with config file:
dodo -c /path/to/config/file/config.json
Usage with all flags:
dodo -c /path/to/config/file/config.json -u https://example.com -m POST -d 10 -r 1000 -t 2000 --no-proxy-check -y`,
Run: func(cmd *cobra.Command, args []string) {},
SilenceErrors: true,
SilenceUsage: true,
Version: config.VERSION,
}
)
rootCmd.Flags().StringVarP(&cliConfig.ConfigFile, "config-file", "c", "", "Path to the config file")
rootCmd.Flags().BoolVarP(&cliConfig.Yes, "yes", "y", false, "Answer yes to all questions")
rootCmd.Flags().StringVarP(&cliConfig.Method, "method", "m", "", fmt.Sprintf("HTTP Method (default %s)", config.DefaultMethod))
rootCmd.Flags().StringVarP(&cliConfig.URL, "url", "u", "", "URL for stress testing")
rootCmd.Flags().UintVarP(&dodosCount, "dodos-count", "d", config.DefaultDodosCount, "Number of dodos(threads)")
rootCmd.Flags().UintVarP(&requestCount, "request-count", "r", config.DefaultRequestCount, "Number of total requests")
rootCmd.Flags().Uint32VarP(&timeout, "timeout", "t", config.DefaultTimeout, "Timeout for each request in milliseconds")
rootCmd.Flags().BoolVar(&noProxyCheck, "no-proxy-check", false, "Do not check for proxies")
if err := rootCmd.Execute(); err != nil {
utils.PrintErr(err)
rootCmd.Println(rootCmd.UsageString())
return nil, customerrors.CobraErrorFormater(err)
}
rootCmd.Flags().Visit(func(f *pflag.Flag) {
switch f.Name {
case "help":
returnNil = true
case "version":
returnNil = true
case "dodos-count":
cliConfig.DodosCount = dodosCount
case "request-count":
cliConfig.RequestCount = requestCount
case "timeout":
cliConfig.Timeout = timeout
case "no-proxy-check":
cliConfig.NoProxyCheck = utils.NewOption(noProxyCheck)
}
})
if returnNil {
return nil, nil
}
return cliConfig, nil
}
// CLIYesOrNoReader reads a yes or no answer from the command line.
// It prompts the user with the given message and default value,
// and returns true if the user answers "y" or "Y", and false otherwise.
// If there is an error while reading the input, it returns false.
// If the user simply presses enter without providing any input,
// it returns the default value specified by the `dft` parameter.
func CLIYesOrNoReader(message string, dft bool) bool {
var answer string
defaultMessage := "Y/n"
if !dft {
defaultMessage = "y/N"
}
fmt.Printf("%s [%s]: ", message, defaultMessage)
if _, err := fmt.Scanln(&answer); err != nil {
if err.Error() == "unexpected newline" {
return dft
}
return false
}
if answer == "" {
return dft
}
return answer == "y" || answer == "Y"
}

View File

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

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

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.MarkAsErrored() dodosTracker.MarkAsDone()
} else {
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 {
totalMaxDuration = response.Time
}
totalDuration += response.Time
if response.Error != nil {
mergedResponses[response.Error.Error()] = append(
mergedResponses[response.Error.Error()],
response.Time,
)
} else {
mergedResponses[fmt.Sprintf("%d", response.StatusCode)] = append(
mergedResponses[fmt.Sprintf("%d", response.StatusCode)],
response.Time,
)
}
} }
mergedResponses := make(map[string]types.Durations)
var allDurations types.Durations
for _, response := range responses {
if response.Time < total.Min {
total.Min = response.Time
}
if response.Time > total.Max {
total.Max = response.Time
}
total.Sum += response.Time
mergedResponses[response.Response] = append(
mergedResponses[response.Response],
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()
} }
t.AppendRow(table.Row{
"Total", if len(mergedResponses) > 1 {
totalCount, t.AppendRow(table.Row{
totalMinDuration, "Total",
totalMaxDuration, total.Count,
totalDuration / time.Duration(totalCount), utils.DurationRoundBy(total.Min, roundPrecision),
}) utils.DurationRoundBy(total.Max, roundPrecision),
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,38 @@ 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 !checkConnection(checkConnectionCtx) {
checkConnectionCtxCancel()
return nil, customerrors.ErrNoInternet
}
checkConnectionCtxCancel()
clients := getClients( clients := getClients(
ctx, ctx,
requestConfig.Timeout, requestConfig.Timeout,
requestConfig.Proxies, requestConfig.Proxies,
requestConfig.GetValidDodosCountForProxies(),
requestConfig.GetMaxConns(fasthttp.DefaultMaxConnsPerHost), requestConfig.GetMaxConns(fasthttp.DefaultMaxConnsPerHost),
requestConfig.Yes,
requestConfig.NoProxyCheck,
requestConfig.URL, requestConfig.URL,
) )
if clients == nil { if clients == nil {
return nil, customerrors.ErrInterrupt return nil, types.ErrInterrupt
} }
responses := releaseDodos(ctx, requestConfig, clients) responses := releaseDodos(ctx, requestConfig, clients)
if ctx.Err() != nil && len(responses) == 0 { if ctx.Err() != nil && len(responses) == 0 {
return nil, customerrors.ErrInterrupt return nil, types.ErrInterrupt
} }
return responses, nil return responses, nil
@ -73,47 +59,59 @@ func releaseDodos(
streamWG sync.WaitGroup streamWG sync.WaitGroup
requestCountPerDodo uint requestCountPerDodo uint
dodosCount uint = requestConfig.GetValidDodosCountForRequests() dodosCount uint = 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)
for i := range dodosCount { if requestConfig.RequestCount == 0 {
if i+1 == dodosCount { for i := range dodosCount {
requestCountPerDodo = requestCount - (i * requestCount / dodosCount) go sendRequest(
} else { ctx,
requestCountPerDodo = ((i + 1) * requestCount / dodosCount) - newRequest(*requestConfig, clients, int64(i)),
(i * requestCount / dodosCount) requestConfig.Timeout,
&responses[i],
increase,
&wg,
)
} }
} else {
for i := range dodosCount {
if i+1 == dodosCount {
requestCountPerDodo = requestConfig.RequestCount - (i * requestConfig.RequestCount / dodosCount)
} else {
requestCountPerDodo = ((i + 1) * requestConfig.RequestCount / dodosCount) -
(i * requestConfig.RequestCount / dodosCount)
}
go sendRequest( go sendRequestByCount(
ctx, ctx,
newRequest(*requestConfig, clients, int64(i)), newRequest(*requestConfig, clients, int64(i)),
requestConfig.Timeout, requestConfig.Timeout,
requestCountPerDodo, requestCountPerDodo,
&responses[i], &responses[i],
increase, increase,
&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,22 +136,67 @@ 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
return return
} }
*responseData = append(*responseData, &Response{ *responseData = append(*responseData, &Response{
StatusCode: response.StatusCode(), Response: strconv.Itoa(response.StatusCode()),
Error: nil, Time: completedTime,
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,
}) })
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 string(buffer.Bytes())
}
if len(body) == 1 {
buffer.WriteString(body[0])
return string(buffer.Bytes())
}
buffer.WriteString(text.FgBlue.Sprint("Random") + "[\n")
indent := " "
displayLimit := 5
for i, item := range body[:min(len(body), displayLimit)] {
if i > 0 {
buffer.WriteString(",\n")
}
buffer.WriteString(indent + item)
}
// Add remaining count if there are more items
if remainingValues := len(body) - displayLimit; remainingValues > 0 {
buffer.WriteString(",\n" + indent + text.FgGreen.Sprintf("+%d bodies", remainingValues))
}
buffer.WriteString("\n]")
return string(buffer.Bytes())
}
func (body *Body) UnmarshalJSON(b []byte) error {
var data any
if err := json.Unmarshal(b, &data); err != nil {
return err
}
switch v := data.(type) {
case string:
*body = []string{v}
case []any:
var slice []string
for _, item := range v {
slice = append(slice, fmt.Sprintf("%v", item))
}
*body = slice
default:
return fmt.Errorf("invalid type for Body: %T (should be string or []string)", v)
}
return nil
}
func (body *Body) UnmarshalYAML(unmarshal func(interface{}) error) error {
var data any
if err := unmarshal(&data); err != nil {
return err
}
switch v := data.(type) {
case string:
*body = []string{v}
case []any:
var slice []string
for _, item := range v {
slice = append(slice, fmt.Sprintf("%v", item))
}
*body = slice
default:
return fmt.Errorf("invalid type for Body: %T (should be string or []string)", v)
}
return nil
}
func (body *Body) Set(value string) error {
*body = append(*body, value)
return nil
}

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

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

6
types/key_value.go Normal file
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 string(buffer.Bytes())
}
indent := " "
displayLimit := 3
for i, item := range params[:min(len(params), displayLimit)] {
if i > 0 {
buffer.WriteString(",\n")
}
if len(item.Value) == 1 {
buffer.WriteString(item.Key + ": " + item.Value[0])
continue
}
buffer.WriteString(item.Key + ": " + text.FgBlue.Sprint("Random") + "[\n")
for ii, v := range item.Value[:min(len(item.Value), displayLimit)] {
if ii == len(item.Value)-1 {
buffer.WriteString(indent + v + "\n")
} else {
buffer.WriteString(indent + v + ",\n")
}
}
// Add remaining values count if needed
if remainingValues := len(item.Value) - displayLimit; remainingValues > 0 {
buffer.WriteString(indent + text.FgGreen.Sprintf("+%d values", remainingValues) + "\n")
}
buffer.WriteString("]")
}
// Add remaining key-value pairs count if needed
if remainingPairs := len(params) - displayLimit; remainingPairs > 0 {
buffer.WriteString(",\n" + text.FgGreen.Sprintf("+%d params", remainingPairs))
}
return string(buffer.Bytes())
}
func (params *Params) AppendByKey(key, value string) {
if item := params.GetValue(key); item != nil {
*item = append(*item, value)
} else {
*params = append(*params, KeyValue[string, []string]{Key: key, Value: []string{value}})
}
}
func (params Params) GetValue(key string) *[]string {
for i := range params {
if params[i].Key == key {
return &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 string(buffer.Bytes())
}
if len(proxies) == 1 {
buffer.WriteString(proxies[0].String())
return string(buffer.Bytes())
}
buffer.WriteString(text.FgBlue.Sprint("Random") + "[\n")
indent := " "
displayLimit := 5
for i, item := range proxies[:min(len(proxies), displayLimit)] {
if i > 0 {
buffer.WriteString(",\n")
}
buffer.WriteString(indent + item.String())
}
// Add remaining count if there are more items
if remainingValues := len(proxies) - displayLimit; remainingValues > 0 {
buffer.WriteString(",\n" + indent + text.FgGreen.Sprintf("+%d proxies", remainingValues))
}
buffer.WriteString("\n]")
return string(buffer.Bytes())
}
func (proxies *Proxies) UnmarshalJSON(b []byte) error {
var data any
if err := json.Unmarshal(b, &data); err != nil {
return err
}
switch v := data.(type) {
case string:
parsed, err := url.Parse(v)
if err != nil {
return err
}
*proxies = []url.URL{*parsed}
case []any:
var urls []url.URL
for _, item := range v {
url, err := url.Parse(item.(string))
if err != nil {
return err
}
urls = append(urls, *url)
}
*proxies = urls
default:
return fmt.Errorf("invalid type for Body: %T (should be URL or []URL)", v)
}
return nil
}
func (proxies *Proxies) UnmarshalYAML(unmarshal func(interface{}) error) error {
var data any
if err := unmarshal(&data); err != nil {
return err
}
switch v := data.(type) {
case string:
parsed, err := url.Parse(v)
if err != nil {
return err
}
*proxies = []url.URL{*parsed}
case []any:
var urls []url.URL
for _, item := range v {
url, err := url.Parse(item.(string))
if err != nil {
return err
}
urls = append(urls, *url)
}
*proxies = urls
default:
return fmt.Errorf("invalid type for Body: %T (should be URL or []URL)", v)
}
return nil
}
func (proxies *Proxies) Set(value string) error {
parsedURL, err := url.Parse(value)
if err != nil {
return err
}
*proxies = append(*proxies, *parsedURL)
return nil
}

59
types/request_url.go Normal file
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.Duration.String())
}
func (timeout *Timeout) UnmarshalYAML(unmarshal func(interface{}) error) error {
var v any
if err := unmarshal(&v); err != nil {
return err
}
switch value := v.(type) {
case float64:
timeout.Duration = time.Duration(value)
return nil
case string:
var err error
timeout.Duration, err = time.ParseDuration(value)
if err != nil {
return errors.New("Timeout is invalid (e.g. 400ms, 1s, 5m, 1h)")
}
return nil
default:
return errors.New("Timeout is invalid (e.g. 400ms, 1s, 5m, 1h)")
}
}

14
utils/compare.go Normal file
View File

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

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.

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,71 +0,0 @@
package utils
import (
"encoding/json"
"errors"
)
type NonNilConcrete interface {
~int | ~float64 | ~string | ~bool
}
type IOption[T NonNilConcrete] interface {
IsNone() bool
ValueOrErr() (*T, error)
ValueOr(def *T) *T
ValueOrPanic() *T
UnmarshalJSON(data []byte) error
}
// Don't call this struct directly, use NewOption[T] or NewNoneOption[T] instead.
type option[T NonNilConcrete] struct {
// value holds the actual value of the Option if it is not None.
value T
// none indicates whether the Option is None (i.e., has no value).
none bool
}
func (o *option[T]) IsNone() bool {
return o.none
}
// The returned value can be nil, if the Option is None, it will return nil and an error.
func (o *option[T]) ValueOrErr() (*T, error) {
if o.IsNone() {
return nil, errors.New("Option is None")
}
return &o.value, nil
}
// The returned value can't be nil, if the Option is None, it will return the default value.
func (o *option[T]) ValueOr(def *T) *T {
if o.IsNone() {
return def
}
return &o.value
}
// The returned value can't be nil, if the Option is None, it will panic.
func (o *option[T]) ValueOrPanic() *T {
if o.IsNone() {
panic("Option is None")
}
return &o.value
}
func (o *option[T]) UnmarshalJSON(data []byte) error {
if string(data) == "null" {
o.none = true
return nil
}
o.none = false
return json.Unmarshal(data, &o.value)
}
func NewOption[T NonNilConcrete](value T) *option[T] {
return &option[T]{value: value}
}
func NewNoneOption[T NonNilConcrete]() *option[T] {
return &option[T]{none: true}
}

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
}