Repository: getanteon/anteon Branch: master Commit: 5cf7df3c6b71 Files: 173 Total size: 710.9 KB Directory structure: gitextract_guep6k27/ ├── .devcontainer/ │ ├── .zshrc │ ├── Dockerfile.dev │ └── devcontainer.json ├── .github/ │ ├── ISSUE_TEMPLATE/ │ │ ├── bug_report.md │ │ └── feature_request.md │ ├── dependabot.yml │ ├── pull_request_template.md │ └── workflows/ │ ├── coverage.yml │ ├── docs.yml │ ├── release.yml │ └── test.yml ├── .gitignore ├── .lycheeignore ├── CODE_OF_CONDUCT.md ├── CONTRIBUTING.md ├── LICENSE ├── README.md ├── SECURITY.md ├── assets/ │ └── ddosify.profile ├── ddosify_engine/ │ ├── .dockerignore │ ├── .golangci.yml │ ├── .goreleaser.yml │ ├── Dockerfile │ ├── Dockerfile.dev │ ├── Dockerfile.release │ ├── Jenkinsfile │ ├── Jenkinsfile_benchmark │ ├── README.md │ ├── completions/ │ │ ├── README.md │ │ └── _ddosify │ ├── config/ │ │ ├── base.go │ │ ├── base_test.go │ │ ├── config_testdata/ │ │ │ ├── benchmark/ │ │ │ │ ├── config_correlation_load_1.json │ │ │ │ ├── config_correlation_load_2.json │ │ │ │ ├── config_correlation_load_3.json │ │ │ │ ├── config_correlation_load_4.json │ │ │ │ ├── config_correlation_load_5.json │ │ │ │ ├── config_distinct_user.json │ │ │ │ ├── config_multipart_inject_100rps.json │ │ │ │ ├── config_multipart_inject_10rps.json │ │ │ │ ├── config_multipart_inject_1krps.json │ │ │ │ ├── config_multipart_inject_200rps.json │ │ │ │ ├── config_multipart_inject_2krps.json │ │ │ │ ├── config_multipart_inject_500rps.json │ │ │ │ ├── config_repeated_user.json │ │ │ │ └── json_payload.json │ │ │ ├── config.json │ │ │ ├── config_auth.json │ │ │ ├── config_capture_environment.json │ │ │ ├── config_data_csv.json │ │ │ ├── config_debug_false.json │ │ │ ├── config_debug_mode.json │ │ │ ├── config_empty.json │ │ │ ├── config_global_envs.json │ │ │ ├── config_incorrect.json │ │ │ ├── config_init_cookies.json │ │ │ ├── config_inject_json.json │ │ │ ├── config_inject_json_dynamic.json │ │ │ ├── config_inject_xml.json │ │ │ ├── config_invalid_capture_env.json │ │ │ ├── config_invalid_target.json │ │ │ ├── config_invalid_user_mode_for_cookies.json │ │ │ ├── config_iteration_count.json │ │ │ ├── config_iteration_count_over_req_count.json │ │ │ ├── config_manual_load.json │ │ │ ├── config_manual_load_override.json │ │ │ ├── config_multipart_err.json │ │ │ ├── config_multipart_payload.json │ │ │ ├── config_payload.json │ │ │ ├── config_protocol.json │ │ │ ├── config_test_assertion_fail.json │ │ │ ├── data_json_payload.json │ │ │ ├── json_payload.json │ │ │ ├── json_payload_dynamic.json │ │ │ ├── payload.txt │ │ │ ├── race_configs/ │ │ │ │ ├── capture_envs.json │ │ │ │ ├── global_envs.json │ │ │ │ ├── step_assertions_stdout.json │ │ │ │ └── step_assertions_stdout_json.json │ │ │ ├── test.csv │ │ │ └── xml_payload.xml │ │ ├── json.go │ │ └── json_test.go │ ├── config_examples/ │ │ ├── assertion/ │ │ │ └── expected_body.json │ │ ├── config.json │ │ └── payload.txt │ ├── core/ │ │ ├── assertion/ │ │ │ ├── base.go │ │ │ ├── service.go │ │ │ └── service_test.go │ │ ├── engine.go │ │ ├── engine_test.go │ │ ├── proxy/ │ │ │ ├── base.go │ │ │ ├── base_test.go │ │ │ └── single.go │ │ ├── report/ │ │ │ ├── aggregator.go │ │ │ ├── aggregator_test.go │ │ │ ├── base.go │ │ │ ├── base_test.go │ │ │ ├── debug.go │ │ │ ├── debug_test.go │ │ │ ├── stdout.go │ │ │ ├── stdoutJson.go │ │ │ ├── stdoutJson_test.go │ │ │ └── stdout_test.go │ │ ├── scenario/ │ │ │ ├── client_pool.go │ │ │ ├── client_pool_cookie_test.go │ │ │ ├── data/ │ │ │ │ ├── csv.go │ │ │ │ └── csv_test.go │ │ │ ├── requester/ │ │ │ │ ├── base.go │ │ │ │ ├── base_test.go │ │ │ │ ├── http.go │ │ │ │ └── http_test.go │ │ │ ├── scripting/ │ │ │ │ ├── assertion/ │ │ │ │ │ ├── assert.go │ │ │ │ │ ├── assert_test.go │ │ │ │ │ ├── ast/ │ │ │ │ │ │ └── ast.go │ │ │ │ │ ├── evaluator/ │ │ │ │ │ │ ├── env.go │ │ │ │ │ │ ├── evaluator.go │ │ │ │ │ │ ├── function.go │ │ │ │ │ │ └── function_test.go │ │ │ │ │ ├── lexer/ │ │ │ │ │ │ ├── lexer.go │ │ │ │ │ │ └── lexer_test.go │ │ │ │ │ ├── parser/ │ │ │ │ │ │ ├── parser.go │ │ │ │ │ │ └── parser_test.go │ │ │ │ │ ├── test_files/ │ │ │ │ │ │ ├── a.txt │ │ │ │ │ │ ├── currencies.json │ │ │ │ │ │ ├── jsonArray.json │ │ │ │ │ │ ├── jsonMap.json │ │ │ │ │ │ └── number.json │ │ │ │ │ └── token/ │ │ │ │ │ └── token.go │ │ │ │ ├── extraction/ │ │ │ │ │ ├── base.go │ │ │ │ │ ├── base_test.go │ │ │ │ │ ├── html.go │ │ │ │ │ ├── html_test.go │ │ │ │ │ ├── json.go │ │ │ │ │ ├── json_test.go │ │ │ │ │ ├── regex.go │ │ │ │ │ ├── regex_test.go │ │ │ │ │ ├── xml.go │ │ │ │ │ └── xml_test.go │ │ │ │ └── injection/ │ │ │ │ ├── dynamic_test.go │ │ │ │ ├── environment.go │ │ │ │ ├── environment_dynamic.go │ │ │ │ ├── environment_test.go │ │ │ │ └── init.go │ │ │ ├── service.go │ │ │ └── service_test.go │ │ ├── types/ │ │ │ ├── error.go │ │ │ ├── hammer.go │ │ │ ├── hammer_test.go │ │ │ ├── regex/ │ │ │ │ ├── regex.go │ │ │ │ └── regex_test.go │ │ │ ├── response.go │ │ │ ├── scenario.go │ │ │ └── scenario_test.go │ │ └── util/ │ │ ├── buffer_pool.go │ │ ├── helper.go │ │ └── pool.go │ ├── go.mod │ ├── go.sum │ ├── main.go │ ├── main_benchmark_test.go │ ├── main_exit_test.go │ ├── main_test.go │ └── scripts/ │ ├── install.sh │ └── testing/ │ └── benchstat.sh └── selfhosted/ ├── README.md ├── VERSION ├── docker-compose.yml ├── init_scripts/ │ ├── influxdb/ │ │ └── 01_influxdb_create_buckets.sh │ ├── postgres/ │ │ └── 01_postgres_create_dbs.sql │ └── prometheus/ │ └── prometheus.yml ├── install.sh └── nginx/ └── default_reverseproxy.conf ================================================ FILE CONTENTS ================================================ ================================================ FILE: .devcontainer/.zshrc ================================================ export ZSH=$HOME/.oh-my-zsh ZSH_THEME="cloud" plugins=( git zsh-autosuggestions ) source $ZSH/oh-my-zsh.sh source /usr/share/doc/fzf/examples/key-bindings.zsh source /usr/share/doc/fzf/examples/completion.zsh alias ll='ls -alF' alias hammer-clean='go clean -testcache' alias hammer-test-n-cover='gotest -coverpkg=./... -coverprofile=coverage.out ./... && go tool cover -func coverage.out' export PATH="$PATH:/go/bin" ================================================ FILE: .devcontainer/Dockerfile.dev ================================================ FROM golang:1.18.1 WORKDIR /workspace COPY go.mod ./ COPY go.sum ./ ENV GOPATH /go ENV GOBIN /go/bin ENV LC_ALL=C.UTF-8 ENV LANG=C.UTF-8 ENV SHELL /bin/zsh RUN apt update && apt install -y git zsh vim fzf locales gcc musl-dev curl iputils-ping telnet graphviz bc jq && rm -rf /var/lib/apt/lists/* RUN sh -c "$(curl -fsSL https://raw.github.com/ohmyzsh/ohmyzsh/master/tools/install.sh)" RUN git clone https://github.com/zsh-users/zsh-syntax-highlighting.git ${ZSH_CUSTOM:-~/.oh-my-zsh/custom}/plugins/zsh-syntax-highlighting RUN git clone https://github.com/zsh-users/zsh-autosuggestions ${ZSH_CUSTOM:-~/.oh-my-zsh/custom}/plugins/zsh-autosuggestions RUN curl -sSfL https://raw.githubusercontent.com/golangci/golangci-lint/master/install.sh | sh -s -- -b $(go env GOPATH)/bin v1.45.2 COPY .devcontainer/.zshrc /root/.zshrc RUN go install -v golang.org/x/tools/gopls@v0.8.3 RUN go install -v github.com/rogpeppe/godef@v1.1.2 RUN go install -v github.com/rakyll/gotest@v0.0.6 RUN go install -v github.com/ramya-rao-a/go-outline@1.0.0 RUN go install -v github.com/go-delve/delve/cmd/dlv@v1.8.1 RUN go install -v golang.org/x/perf/cmd/benchstat@v0.0.0-20221222172245-91a04616dc65 RUN go mod download CMD [ "zsh" ] ================================================ FILE: .devcontainer/devcontainer.json ================================================ { "name": "Ddosify Open Source", "build": { "dockerfile": "Dockerfile.dev", "context": "../" }, "runArgs": [ "-v", "${env:HOME}${env:USERPROFILE}/.ssh:/root/.ssh-localhost:ro", "--cap-add=SYS_PTRACE", "--security-opt", "seccomp=unconfined", "--ipc", "host", "--hostname", "ddosify" ], "customizations": { "vscode": { "settings": { "terminal.integrated.defaultProfile.linux": "zsh", "go.useLanguageServer": true, "go.gopath": "/go", "go.goroot": "/usr/local/go", "go.toolsGopath": "/go", "go.lintTool": "golangci-lint", "go.lintFlags": [ "--config=${workspaceFolder}/.golangci.yml", "--fast" ], "files.eol": "\n" }, "extensions": [ "golang.Go", "eamodio.gitlens", "premparihar.gotestexplorer", "GitHub.copilot", "GitHub.copilot-labs" ] } }, "postCreateCommand": "mkdir -p ~/.ssh && cp -r ~/.ssh-localhost/* ~/.ssh && chmod 700 ~/.ssh && chmod 600 ~/.ssh/*", "mounts": [ // "source=${env:HOME}/.zsh_history,target=/root/.zsh_history,type=bind,consistency=cached" // "source=${env:HOME}/.bash_history,target=/root/.zsh_history,type=bind,consistency=cached" ] } ================================================ FILE: .github/ISSUE_TEMPLATE/bug_report.md ================================================ --- name: Bug report about: Create a report to help us improve title: '' labels: bug assignees: '' --- ### Describe the bug ### To Reproduce Steps to reproduce the behavior: 1. Go to '...' 2. Click on '....' 3. Scroll down to '....' 4. See error ### Expected behavior ### Screenshots ### System (please complete the following information): - OS: [e.g. MacOS] - Anteon Version [e.g. v0.15.1] ### Additional context ================================================ FILE: .github/ISSUE_TEMPLATE/feature_request.md ================================================ --- name: Feature request about: Suggest an idea for this project title: '' labels: enhancement assignees: '' --- **Is your feature request related to a problem? Please describe.** **Describe the solution you'd like** **Describe alternatives you've considered** **Additional context** ================================================ FILE: .github/dependabot.yml ================================================ # To get started with Dependabot version updates, you'll need to specify which # package ecosystems to update and where the package manifests are located. # Please see the documentation for all configuration options: # https://docs.github.com/code-security/dependabot/dependabot-version-updates/configuration-options-for-the-dependabot.yml-file version: 2 updates: - package-ecosystem: "gomod" # See documentation for possible values directory: "/" # Location of package manifests schedule: interval: "daily" - package-ecosystem: "docker" # See documentation for possible values directory: "/" # Location of package manifests schedule: interval: "daily" ================================================ FILE: .github/pull_request_template.md ================================================ ## Description ## Screenshots ## Type of Changes - [ ] Bug fix (non-breaking change which fixes an issue) - [ ] New feature (non-breaking change which adds functionality) - [ ] Breaking change (fix or feature that would cause existing functionality to change) - [ ] This change requires a documentation update ## Checklist - [ ] I have read the [CONTRIBUTING.md](../CONTRIBUTING.md) document. - [ ] My code follows the code style of this project. - [ ] I have added tests to cover my changes. - [ ] All new and existing tests passed. - [ ] I have updated the [README.md](../README.md) as necessary if there are changes. - [ ] I have tested the changes on my local machine before submitting the PR. ================================================ FILE: .github/workflows/coverage.yml ================================================ name: Coverage on: push: branches: - master - develop pull_request: branches: - master - develop jobs: coverage: runs-on: ubuntu-latest steps: - uses: actions/checkout@v2 - name: Set up Go uses: actions/setup-go@v2 with: go-version: 1.18.x - name: Test run: cd ddosify_engine && go test -coverpkg=./... -coverprofile=coverage.txt -parallel 1 -covermode=atomic -short ./... && go tool cover -func coverage.txt - name: Upload reports to codecov run: | curl -Os https://uploader.codecov.io/latest/linux/codecov chmod +x codecov ./codecov -t ${CODECOV_TOKEN} -f coverage.txt env: CODECOV_TOKEN: ${{ secrets.CODECOV_TOKEN }} ================================================ FILE: .github/workflows/docs.yml ================================================ name: Documentation on: push: branches: - master - develop pull_request: branches: - master - develop jobs: link-checker: name: Check links runs-on: ubuntu-latest steps: - name: Checkout the repository uses: actions/checkout@v4 - name: Check the links uses: lycheeverse/lychee-action@v1 with: args: --max-concurrency 1 -v *.md **/*.md fail: true env: GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} ================================================ FILE: .github/workflows/release.yml ================================================ name: Release Ddosify on: push: tags: - '*' permissions: contents: write jobs: release: runs-on: ubuntu-latest env: DOCKER_CLI_EXPERIMENTAL: "enabled" steps: - name: Checkout uses: actions/checkout@v2 with: fetch-depth: 0 - name: QEMU uses: docker/setup-qemu-action@v1 - name: Docker Buildx uses: docker/setup-buildx-action@v1 - name: Set up Go uses: actions/setup-go@v2 with: go-version: 1.18 - name: Docker Hub Login uses: docker/login-action@v1 with: username: ${{ secrets.DOCKER_USERNAME }} password: ${{ secrets.DOCKER_PASSWORD }} - name: Run GoReleaser uses: goreleaser/goreleaser-action@v2 with: distribution: goreleaser version: latest args: release --clean env: GITHUB_TOKEN: ${{ secrets.ACCESS_TOKEN }} ================================================ FILE: .github/workflows/test.yml ================================================ name: Test on: push: branches: - master - develop pull_request: branches: - master - develop jobs: test: strategy: matrix: go-version: [1.18.x, 1.19.x] os: [ubuntu-latest, macos-latest, windows-latest] runs-on: ${{ matrix.os }} steps: - uses: actions/checkout@v2 - name: Set up Go uses: actions/setup-go@v2 with: go-version: ${{ matrix.go-version }} - name: Build run: cd ddosify_engine && go build -race ./... - name: Test run: cd ddosify_engine && go test -parallel 1 -short ./... ================================================ FILE: .gitignore ================================================ # Binaries for programs and plugins *.exe *.exe~ *.dll *.so *.dylib # Test binary, built with `go test -c` *.test # Output of the go coverage tool, specifically when used with LiteIDE *.out # Dependency directories (remove the comment below to include it) # vendor/ __debug* coverage.html main dist/ ddosify .vscode .idea/ .DS_Store ================================================ FILE: .lycheeignore ================================================ https://getanteon.com/endpoint_1 https://getanteon.com/endpoint_2 http://localhost:8014/ https://gurubase.io/ ================================================ FILE: CODE_OF_CONDUCT.md ================================================ # Contributor Covenant Code of Conduct ## Our Pledge We as members, contributors, and leaders pledge to make participation in our community a harassment-free experience for everyone, regardless of age, body size, visible or invisible disability, ethnicity, sex characteristics, gender identity and expression, level of experience, education, socio-economic status, nationality, personal appearance, race, religion, or sexual identity and orientation. We pledge to act and interact in ways that contribute to an open, welcoming, diverse, inclusive, and healthy community. ## Our Standards Examples of behavior that contributes to a positive environment for our community include: - Demonstrating empathy and kindness toward other people - Being respectful of differing opinions, viewpoints, and experiences - Giving and gracefully accepting constructive feedback - Accepting responsibility and apologizing to those affected by our mistakes, and learning from the experience - Focusing on what is best not just for us as individuals, but for the overall community Examples of unacceptable behavior include: - The use of sexualized language or imagery, and sexual attention or advances of any kind - Trolling, insulting or derogatory comments, and personal or political attacks - Public or private harassment - Publishing others' private information, such as a physical or email address, without their explicit permission - Other conduct which could reasonably be considered inappropriate in a professional setting ## Enforcement Responsibilities Community leaders are responsible for clarifying and enforcing our standards of acceptable behavior and will take appropriate and fair corrective action in response to any behavior that they deem inappropriate, threatening, offensive, or harmful. Community leaders have the right and responsibility to remove, edit, or reject comments, commits, code, wiki edits, issues, and other contributions that are not aligned to this Code of Conduct, and will communicate reasons for moderation decisions when appropriate. ## Scope This Code of Conduct applies within all community spaces, and also applies when an individual is officially representing the community in public spaces. Examples of representing our community include using an official e-mail address, posting via an official social media account, or acting as an appointed representative at an online or offline event. ## Enforcement Instances of abusive, harassing, or otherwise unacceptable behavior may be reported to the community leaders responsible for enforcement at info@getanteon.com. All complaints will be reviewed and investigated promptly and fairly. All community leaders are obligated to respect the privacy and security of the reporter of any incident. ## Enforcement Guidelines Community leaders will follow these Community Impact Guidelines in determining the consequences for any action they deem in violation of this Code of Conduct: ### 1. Correction **Community Impact**: Use of inappropriate language or other behavior deemed unprofessional or unwelcome in the community. **Consequence**: A private, written warning from community leaders, providing clarity around the nature of the violation and an explanation of why the behavior was inappropriate. A public apology may be requested. ### 2. Warning **Community Impact**: A violation through a single incident or series of actions. **Consequence**: A warning with consequences for continued behavior. No interaction with the people involved, including unsolicited interaction with those enforcing the Code of Conduct, for a specified period of time. This includes avoiding interactions in community spaces as well as external channels like social media. Violating these terms may lead to a temporary or permanent ban. ### 3. Temporary Ban **Community Impact**: A serious violation of community standards, including sustained inappropriate behavior. **Consequence**: A temporary ban from any sort of interaction or public communication with the community for a specified period of time. No public or private interaction with the people involved, including unsolicited interaction with those enforcing the Code of Conduct, is allowed during this period. Violating these terms may lead to a permanent ban. ### 4. Permanent Ban **Community Impact**: Demonstrating a pattern of violation of community standards, including sustained inappropriate behavior, harassment of an individual, or aggression toward or disparagement of classes of individuals. **Consequence**: A permanent ban from any sort of public interaction within the community. ## Attribution This Code of Conduct is adapted from the [Contributor Covenant][homepage], version 2.0, available at https://www.contributor-covenant.org/version/2/0/code_of_conduct.html. Community Impact Guidelines were inspired by [Mozilla's code of conduct enforcement ladder](https://github.com/mozilla/diversity). [homepage]: https://www.contributor-covenant.org For answers to common questions about this code of conduct, see the FAQ at https://www.contributor-covenant.org/faq. Translations are available at https://www.contributor-covenant.org/translations. ================================================ FILE: CONTRIBUTING.md ================================================ # Contributing to Anteon 🐝 Thank you for your interest in contributing to [Anteon](https://github.com/getanteon/anteon)! In this guide, we'll provide you with the necessary information and guidelines to help you get started. ## 🚀 Getting Started 1. Fork [Anteon](https://github.com/getanteon/anteon) on GitHub. 2. Clone your fork to your local machine: ```bash git clone git@github.com:/anteon.git ``` 3. Add the Anteon repository as an upstream remote: ```bash git remote add upstream https://github.com/getanteon/anteon ``` 4. We follow Gitflow branching model. Create a feature branch from the `develop` branch: ```bash git checkout -b feature/FEATURE_NAME develop ``` 5. Set up your development environment. - Go programming language (`Version >= 1.18`) is required to build and run Anteon. You can find the installation instructions [here](https://go.dev/doc/install). - We also provide [Dockerfile](./.devcontainer/Dockerfile.dev) and Visual Studio Code (VS Code) [remote container configuration](./.devcontainer/devcontainer.json) for development. More information about VS Code remote container can be found [here](https://code.visualstudio.com/docs/devcontainers/containers). 6. Run the `main.go` file: ```bash go run main.go ``` ## 💻 Submitting Changes Before submitting a [pull request (PR)](https://github.com/getanteon/anteon/pulls) with your changes, please make sure you follow these guidelines: 1. Ensure your code is well-formatted and follows the established coding style for this project (e.g., proper indentation, naming conventions, etc.). 2. Write unit tests for any new functionality or bug fixes. Ensure that all tests pass before submitting your PR. 3. Update the [README.md](./README.md) file according to your changes. 4. Keep your PRs focused and as small as possible. If you have multiple unrelated changes, create separate PRs for them. 5. Add a descriptive title and detailed description to your PR, explaining the purpose and rationale behind your changes. 6. Rebase your branch with the latest upstream changes before submitting your PR: ```bash git pull --rebase upstream master ``` 7. Create a pull request (PR) against the `develop` branch. After submitting your PR, our team will review your changes. We may ask for revisions or provide feedback before merging your changes into the master branch. Your patience and cooperation are greatly appreciated. ## 🐛 Bug Reports When submitting a [bug report](https://github.com/getanteon/anteon/issues), please include: - A clear and descriptive title. - A detailed description of the issue, including the steps to reproduce the bug. - Any relevant information about your environment, such as the OS, Go version, and configuration. - If possible, attach a minimal code sample or test case that demonstrates the issue. - If possible, attach a screenshot or animated GIF that demonstrates the issue. ## ✨ Feature Requests When submitting a [feature request](https://github.com/getanteon/anteon/issues), please include: - A clear and descriptive title. - A detailed description of the proposed feature or enhancement, including the rationale behind it and any potential use cases. - If possible, provide examples or mockups to help illustrate your proposal. ## 💬 Community Join our [Discord Server](https://discord.com/invite/9KdnrSUZQg) for issues, feature requests, feedbacks or anything else. We're happy to help you out! ## 📜 Code of Conduct By participating in this project, you agree to abide by our [Code of Conduct](./CODE_OF_CONDUCT.md). Please read it carefully and ensure that your contributions and interactions with the community adhere to its principles. ================================================ FILE: LICENSE ================================================ GNU AFFERO GENERAL PUBLIC LICENSE Version 3, 19 November 2007 Copyright (C) 2007 Free Software Foundation, Inc. Everyone is permitted to copy and distribute verbatim copies of this license document, but changing it is not allowed. Preamble The GNU Affero General Public License is a free, copyleft license for software and other kinds of works, specifically designed to ensure cooperation with the community in the case of network server software. The licenses for most software and other practical works are designed to take away your freedom to share and change the works. By contrast, our General Public Licenses are intended to guarantee your freedom to share and change all versions of a program--to make sure it remains free software for all its users. When we speak of free software, we are referring to freedom, not price. Our General Public Licenses are designed to make sure that you have the freedom to distribute copies of free software (and charge for them if you wish), that you receive source code or can get it if you want it, that you can change the software or use pieces of it in new free programs, and that you know you can do these things. Developers that use our General Public Licenses protect your rights with two steps: (1) assert copyright on the software, and (2) offer you this License which gives you legal permission to copy, distribute and/or modify the software. A secondary benefit of defending all users' freedom is that improvements made in alternate versions of the program, if they receive widespread use, become available for other developers to incorporate. Many developers of free software are heartened and encouraged by the resulting cooperation. However, in the case of software used on network servers, this result may fail to come about. The GNU General Public License permits making a modified version and letting the public access it on a server without ever releasing its source code to the public. The GNU Affero General Public License is designed specifically to ensure that, in such cases, the modified source code becomes available to the community. It requires the operator of a network server to provide the source code of the modified version running there to the users of that server. Therefore, public use of a modified version, on a publicly accessible server, gives the public access to the source code of the modified version. An older license, called the Affero General Public License and published by Affero, was designed to accomplish similar goals. This is a different license, not a version of the Affero GPL, but Affero has released a new version of the Affero GPL which permits relicensing under this license. The precise terms and conditions for copying, distribution and modification follow. TERMS AND CONDITIONS 0. Definitions. "This License" refers to version 3 of the GNU Affero General Public License. "Copyright" also means copyright-like laws that apply to other kinds of works, such as semiconductor masks. "The Program" refers to any copyrightable work licensed under this License. Each licensee is addressed as "you". "Licensees" and "recipients" may be individuals or organizations. To "modify" a work means to copy from or adapt all or part of the work in a fashion requiring copyright permission, other than the making of an exact copy. The resulting work is called a "modified version" of the earlier work or a work "based on" the earlier work. A "covered work" means either the unmodified Program or a work based on the Program. To "propagate" a work means to do anything with it that, without permission, would make you directly or secondarily liable for infringement under applicable copyright law, except executing it on a computer or modifying a private copy. Propagation includes copying, distribution (with or without modification), making available to the public, and in some countries other activities as well. To "convey" a work means any kind of propagation that enables other parties to make or receive copies. Mere interaction with a user through a computer network, with no transfer of a copy, is not conveying. An interactive user interface displays "Appropriate Legal Notices" to the extent that it includes a convenient and prominently visible feature that (1) displays an appropriate copyright notice, and (2) tells the user that there is no warranty for the work (except to the extent that warranties are provided), that licensees may convey the work under this License, and how to view a copy of this License. If the interface presents a list of user commands or options, such as a menu, a prominent item in the list meets this criterion. 1. Source Code. The "source code" for a work means the preferred form of the work for making modifications to it. "Object code" means any non-source form of a work. A "Standard Interface" means an interface that either is an official standard defined by a recognized standards body, or, in the case of interfaces specified for a particular programming language, one that is widely used among developers working in that language. The "System Libraries" of an executable work include anything, other than the work as a whole, that (a) is included in the normal form of packaging a Major Component, but which is not part of that Major Component, and (b) serves only to enable use of the work with that Major Component, or to implement a Standard Interface for which an implementation is available to the public in source code form. A "Major Component", in this context, means a major essential component (kernel, window system, and so on) of the specific operating system (if any) on which the executable work runs, or a compiler used to produce the work, or an object code interpreter used to run it. The "Corresponding Source" for a work in object code form means all the source code needed to generate, install, and (for an executable work) run the object code and to modify the work, including scripts to control those activities. However, it does not include the work's System Libraries, or general-purpose tools or generally available free programs which are used unmodified in performing those activities but which are not part of the work. For example, Corresponding Source includes interface definition files associated with source files for the work, and the source code for shared libraries and dynamically linked subprograms that the work is specifically designed to require, such as by intimate data communication or control flow between those subprograms and other parts of the work. The Corresponding Source need not include anything that users can regenerate automatically from other parts of the Corresponding Source. The Corresponding Source for a work in source code form is that same work. 2. Basic Permissions. All rights granted under this License are granted for the term of copyright on the Program, and are irrevocable provided the stated conditions are met. This License explicitly affirms your unlimited permission to run the unmodified Program. The output from running a covered work is covered by this License only if the output, given its content, constitutes a covered work. This License acknowledges your rights of fair use or other equivalent, as provided by copyright law. You may make, run and propagate covered works that you do not convey, without conditions so long as your license otherwise remains in force. You may convey covered works to others for the sole purpose of having them make modifications exclusively for you, or provide you with facilities for running those works, provided that you comply with the terms of this License in conveying all material for which you do not control copyright. Those thus making or running the covered works for you must do so exclusively on your behalf, under your direction and control, on terms that prohibit them from making any copies of your copyrighted material outside their relationship with you. Conveying under any other circumstances is permitted solely under the conditions stated below. Sublicensing is not allowed; section 10 makes it unnecessary. 3. Protecting Users' Legal Rights From Anti-Circumvention Law. No covered work shall be deemed part of an effective technological measure under any applicable law fulfilling obligations under article 11 of the WIPO copyright treaty adopted on 20 December 1996, or similar laws prohibiting or restricting circumvention of such measures. When you convey a covered work, you waive any legal power to forbid circumvention of technological measures to the extent such circumvention is effected by exercising rights under this License with respect to the covered work, and you disclaim any intention to limit operation or modification of the work as a means of enforcing, against the work's users, your or third parties' legal rights to forbid circumvention of technological measures. 4. Conveying Verbatim Copies. You may convey verbatim copies of the Program's source code as you receive it, in any medium, provided that you conspicuously and appropriately publish on each copy an appropriate copyright notice; keep intact all notices stating that this License and any non-permissive terms added in accord with section 7 apply to the code; keep intact all notices of the absence of any warranty; and give all recipients a copy of this License along with the Program. You may charge any price or no price for each copy that you convey, and you may offer support or warranty protection for a fee. 5. Conveying Modified Source Versions. You may convey a work based on the Program, or the modifications to produce it from the Program, in the form of source code under the terms of section 4, provided that you also meet all of these conditions: a) The work must carry prominent notices stating that you modified it, and giving a relevant date. b) The work must carry prominent notices stating that it is released under this License and any conditions added under section 7. This requirement modifies the requirement in section 4 to "keep intact all notices". c) You must license the entire work, as a whole, under this License to anyone who comes into possession of a copy. This License will therefore apply, along with any applicable section 7 additional terms, to the whole of the work, and all its parts, regardless of how they are packaged. This License gives no permission to license the work in any other way, but it does not invalidate such permission if you have separately received it. d) If the work has interactive user interfaces, each must display Appropriate Legal Notices; however, if the Program has interactive interfaces that do not display Appropriate Legal Notices, your work need not make them do so. A compilation of a covered work with other separate and independent works, which are not by their nature extensions of the covered work, and which are not combined with it such as to form a larger program, in or on a volume of a storage or distribution medium, is called an "aggregate" if the compilation and its resulting copyright are not used to limit the access or legal rights of the compilation's users beyond what the individual works permit. Inclusion of a covered work in an aggregate does not cause this License to apply to the other parts of the aggregate. 6. Conveying Non-Source Forms. You may convey a covered work in object code form under the terms of sections 4 and 5, provided that you also convey the machine-readable Corresponding Source under the terms of this License, in one of these ways: a) Convey the object code in, or embodied in, a physical product (including a physical distribution medium), accompanied by the Corresponding Source fixed on a durable physical medium customarily used for software interchange. b) Convey the object code in, or embodied in, a physical product (including a physical distribution medium), accompanied by a written offer, valid for at least three years and valid for as long as you offer spare parts or customer support for that product model, to give anyone who possesses the object code either (1) a copy of the Corresponding Source for all the software in the product that is covered by this License, on a durable physical medium customarily used for software interchange, for a price no more than your reasonable cost of physically performing this conveying of source, or (2) access to copy the Corresponding Source from a network server at no charge. c) Convey individual copies of the object code with a copy of the written offer to provide the Corresponding Source. This alternative is allowed only occasionally and noncommercially, and only if you received the object code with such an offer, in accord with subsection 6b. d) Convey the object code by offering access from a designated place (gratis or for a charge), and offer equivalent access to the Corresponding Source in the same way through the same place at no further charge. You need not require recipients to copy the Corresponding Source along with the object code. If the place to copy the object code is a network server, the Corresponding Source may be on a different server (operated by you or a third party) that supports equivalent copying facilities, provided you maintain clear directions next to the object code saying where to find the Corresponding Source. Regardless of what server hosts the Corresponding Source, you remain obligated to ensure that it is available for as long as needed to satisfy these requirements. e) Convey the object code using peer-to-peer transmission, provided you inform other peers where the object code and Corresponding Source of the work are being offered to the general public at no charge under subsection 6d. A separable portion of the object code, whose source code is excluded from the Corresponding Source as a System Library, need not be included in conveying the object code work. A "User Product" is either (1) a "consumer product", which means any tangible personal property which is normally used for personal, family, or household purposes, or (2) anything designed or sold for incorporation into a dwelling. In determining whether a product is a consumer product, doubtful cases shall be resolved in favor of coverage. For a particular product received by a particular user, "normally used" refers to a typical or common use of that class of product, regardless of the status of the particular user or of the way in which the particular user actually uses, or expects or is expected to use, the product. A product is a consumer product regardless of whether the product has substantial commercial, industrial or non-consumer uses, unless such uses represent the only significant mode of use of the product. "Installation Information" for a User Product means any methods, procedures, authorization keys, or other information required to install and execute modified versions of a covered work in that User Product from a modified version of its Corresponding Source. The information must suffice to ensure that the continued functioning of the modified object code is in no case prevented or interfered with solely because modification has been made. If you convey an object code work under this section in, or with, or specifically for use in, a User Product, and the conveying occurs as part of a transaction in which the right of possession and use of the User Product is transferred to the recipient in perpetuity or for a fixed term (regardless of how the transaction is characterized), the Corresponding Source conveyed under this section must be accompanied by the Installation Information. But this requirement does not apply if neither you nor any third party retains the ability to install modified object code on the User Product (for example, the work has been installed in ROM). The requirement to provide Installation Information does not include a requirement to continue to provide support service, warranty, or updates for a work that has been modified or installed by the recipient, or for the User Product in which it has been modified or installed. Access to a network may be denied when the modification itself materially and adversely affects the operation of the network or violates the rules and protocols for communication across the network. Corresponding Source conveyed, and Installation Information provided, in accord with this section must be in a format that is publicly documented (and with an implementation available to the public in source code form), and must require no special password or key for unpacking, reading or copying. 7. Additional Terms. "Additional permissions" are terms that supplement the terms of this License by making exceptions from one or more of its conditions. Additional permissions that are applicable to the entire Program shall be treated as though they were included in this License, to the extent that they are valid under applicable law. If additional permissions apply only to part of the Program, that part may be used separately under those permissions, but the entire Program remains governed by this License without regard to the additional permissions. When you convey a copy of a covered work, you may at your option remove any additional permissions from that copy, or from any part of it. (Additional permissions may be written to require their own removal in certain cases when you modify the work.) You may place additional permissions on material, added by you to a covered work, for which you have or can give appropriate copyright permission. Notwithstanding any other provision of this License, for material you add to a covered work, you may (if authorized by the copyright holders of that material) supplement the terms of this License with terms: a) Disclaiming warranty or limiting liability differently from the terms of sections 15 and 16 of this License; or b) Requiring preservation of specified reasonable legal notices or author attributions in that material or in the Appropriate Legal Notices displayed by works containing it; or c) Prohibiting misrepresentation of the origin of that material, or requiring that modified versions of such material be marked in reasonable ways as different from the original version; or d) Limiting the use for publicity purposes of names of licensors or authors of the material; or e) Declining to grant rights under trademark law for use of some trade names, trademarks, or service marks; or f) Requiring indemnification of licensors and authors of that material by anyone who conveys the material (or modified versions of it) with contractual assumptions of liability to the recipient, for any liability that these contractual assumptions directly impose on those licensors and authors. All other non-permissive additional terms are considered "further restrictions" within the meaning of section 10. If the Program as you received it, or any part of it, contains a notice stating that it is governed by this License along with a term that is a further restriction, you may remove that term. If a license document contains a further restriction but permits relicensing or conveying under this License, you may add to a covered work material governed by the terms of that license document, provided that the further restriction does not survive such relicensing or conveying. If you add terms to a covered work in accord with this section, you must place, in the relevant source files, a statement of the additional terms that apply to those files, or a notice indicating where to find the applicable terms. Additional terms, permissive or non-permissive, may be stated in the form of a separately written license, or stated as exceptions; the above requirements apply either way. 8. Termination. You may not propagate or modify a covered work except as expressly provided under this License. Any attempt otherwise to propagate or modify it is void, and will automatically terminate your rights under this License (including any patent licenses granted under the third paragraph of section 11). However, if you cease all violation of this License, then your license from a particular copyright holder is reinstated (a) provisionally, unless and until the copyright holder explicitly and finally terminates your license, and (b) permanently, if the copyright holder fails to notify you of the violation by some reasonable means prior to 60 days after the cessation. Moreover, your license from a particular copyright holder is reinstated permanently if the copyright holder notifies you of the violation by some reasonable means, this is the first time you have received notice of violation of this License (for any work) from that copyright holder, and you cure the violation prior to 30 days after your receipt of the notice. Termination of your rights under this section does not terminate the licenses of parties who have received copies or rights from you under this License. If your rights have been terminated and not permanently reinstated, you do not qualify to receive new licenses for the same material under section 10. 9. Acceptance Not Required for Having Copies. You are not required to accept this License in order to receive or run a copy of the Program. Ancillary propagation of a covered work occurring solely as a consequence of using peer-to-peer transmission to receive a copy likewise does not require acceptance. However, nothing other than this License grants you permission to propagate or modify any covered work. These actions infringe copyright if you do not accept this License. Therefore, by modifying or propagating a covered work, you indicate your acceptance of this License to do so. 10. Automatic Licensing of Downstream Recipients. Each time you convey a covered work, the recipient automatically receives a license from the original licensors, to run, modify and propagate that work, subject to this License. You are not responsible for enforcing compliance by third parties with this License. An "entity transaction" is a transaction transferring control of an organization, or substantially all assets of one, or subdividing an organization, or merging organizations. If propagation of a covered work results from an entity transaction, each party to that transaction who receives a copy of the work also receives whatever licenses to the work the party's predecessor in interest had or could give under the previous paragraph, plus a right to possession of the Corresponding Source of the work from the predecessor in interest, if the predecessor has it or can get it with reasonable efforts. You may not impose any further restrictions on the exercise of the rights granted or affirmed under this License. For example, you may not impose a license fee, royalty, or other charge for exercise of rights granted under this License, and you may not initiate litigation (including a cross-claim or counterclaim in a lawsuit) alleging that any patent claim is infringed by making, using, selling, offering for sale, or importing the Program or any portion of it. 11. Patents. A "contributor" is a copyright holder who authorizes use under this License of the Program or a work on which the Program is based. The work thus licensed is called the contributor's "contributor version". A contributor's "essential patent claims" are all patent claims owned or controlled by the contributor, whether already acquired or hereafter acquired, that would be infringed by some manner, permitted by this License, of making, using, or selling its contributor version, but do not include claims that would be infringed only as a consequence of further modification of the contributor version. For purposes of this definition, "control" includes the right to grant patent sublicenses in a manner consistent with the requirements of this License. Each contributor grants you a non-exclusive, worldwide, royalty-free patent license under the contributor's essential patent claims, to make, use, sell, offer for sale, import and otherwise run, modify and propagate the contents of its contributor version. In the following three paragraphs, a "patent license" is any express agreement or commitment, however denominated, not to enforce a patent (such as an express permission to practice a patent or covenant not to sue for patent infringement). To "grant" such a patent license to a party means to make such an agreement or commitment not to enforce a patent against the party. If you convey a covered work, knowingly relying on a patent license, and the Corresponding Source of the work is not available for anyone to copy, free of charge and under the terms of this License, through a publicly available network server or other readily accessible means, then you must either (1) cause the Corresponding Source to be so available, or (2) arrange to deprive yourself of the benefit of the patent license for this particular work, or (3) arrange, in a manner consistent with the requirements of this License, to extend the patent license to downstream recipients. "Knowingly relying" means you have actual knowledge that, but for the patent license, your conveying the covered work in a country, or your recipient's use of the covered work in a country, would infringe one or more identifiable patents in that country that you have reason to believe are valid. If, pursuant to or in connection with a single transaction or arrangement, you convey, or propagate by procuring conveyance of, a covered work, and grant a patent license to some of the parties receiving the covered work authorizing them to use, propagate, modify or convey a specific copy of the covered work, then the patent license you grant is automatically extended to all recipients of the covered work and works based on it. A patent license is "discriminatory" if it does not include within the scope of its coverage, prohibits the exercise of, or is conditioned on the non-exercise of one or more of the rights that are specifically granted under this License. You may not convey a covered work if you are a party to an arrangement with a third party that is in the business of distributing software, under which you make payment to the third party based on the extent of your activity of conveying the work, and under which the third party grants, to any of the parties who would receive the covered work from you, a discriminatory patent license (a) in connection with copies of the covered work conveyed by you (or copies made from those copies), or (b) primarily for and in connection with specific products or compilations that contain the covered work, unless you entered into that arrangement, or that patent license was granted, prior to 28 March 2007. Nothing in this License shall be construed as excluding or limiting any implied license or other defenses to infringement that may otherwise be available to you under applicable patent law. 12. No Surrender of Others' Freedom. If conditions are imposed on you (whether by court order, agreement or otherwise) that contradict the conditions of this License, they do not excuse you from the conditions of this License. If you cannot convey a covered work so as to satisfy simultaneously your obligations under this License and any other pertinent obligations, then as a consequence you may not convey it at all. For example, if you agree to terms that obligate you to collect a royalty for further conveying from those to whom you convey the Program, the only way you could satisfy both those terms and this License would be to refrain entirely from conveying the Program. 13. Remote Network Interaction; Use with the GNU General Public License. Notwithstanding any other provision of this License, if you modify the Program, your modified version must prominently offer all users interacting with it remotely through a computer network (if your version supports such interaction) an opportunity to receive the Corresponding Source of your version by providing access to the Corresponding Source from a network server at no charge, through some standard or customary means of facilitating copying of software. This Corresponding Source shall include the Corresponding Source for any work covered by version 3 of the GNU General Public License that is incorporated pursuant to the following paragraph. Notwithstanding any other provision of this License, you have permission to link or combine any covered work with a work licensed under version 3 of the GNU General Public License into a single combined work, and to convey the resulting work. The terms of this License will continue to apply to the part which is the covered work, but the work with which it is combined will remain governed by version 3 of the GNU General Public License. 14. Revised Versions of this License. The Free Software Foundation may publish revised and/or new versions of the GNU Affero General Public License from time to time. Such new versions will be similar in spirit to the present version, but may differ in detail to address new problems or concerns. Each version is given a distinguishing version number. If the Program specifies that a certain numbered version of the GNU Affero General Public License "or any later version" applies to it, you have the option of following the terms and conditions either of that numbered version or of any later version published by the Free Software Foundation. If the Program does not specify a version number of the GNU Affero General Public License, you may choose any version ever published by the Free Software Foundation. If the Program specifies that a proxy can decide which future versions of the GNU Affero General Public License can be used, that proxy's public statement of acceptance of a version permanently authorizes you to choose that version for the Program. Later license versions may give you additional or different permissions. However, no additional obligations are imposed on any author or copyright holder as a result of your choosing to follow a later version. 15. Disclaimer of Warranty. THERE IS NO WARRANTY FOR THE PROGRAM, TO THE EXTENT PERMITTED BY APPLICABLE LAW. EXCEPT WHEN OTHERWISE STATED IN WRITING THE COPYRIGHT HOLDERS AND/OR OTHER PARTIES PROVIDE THE PROGRAM "AS IS" WITHOUT WARRANTY OF ANY KIND, EITHER EXPRESSED OR IMPLIED, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE. THE ENTIRE RISK AS TO THE QUALITY AND PERFORMANCE OF THE PROGRAM IS WITH YOU. SHOULD THE PROGRAM PROVE DEFECTIVE, YOU ASSUME THE COST OF ALL NECESSARY SERVICING, REPAIR OR CORRECTION. 16. Limitation of Liability. IN NO EVENT UNLESS REQUIRED BY APPLICABLE LAW OR AGREED TO IN WRITING WILL ANY COPYRIGHT HOLDER, OR ANY OTHER PARTY WHO MODIFIES AND/OR CONVEYS THE PROGRAM AS PERMITTED ABOVE, BE LIABLE TO YOU FOR DAMAGES, INCLUDING ANY GENERAL, SPECIAL, INCIDENTAL OR CONSEQUENTIAL DAMAGES ARISING OUT OF THE USE OR INABILITY TO USE THE PROGRAM (INCLUDING BUT NOT LIMITED TO LOSS OF DATA OR DATA BEING RENDERED INACCURATE OR LOSSES SUSTAINED BY YOU OR THIRD PARTIES OR A FAILURE OF THE PROGRAM TO OPERATE WITH ANY OTHER PROGRAMS), EVEN IF SUCH HOLDER OR OTHER PARTY HAS BEEN ADVISED OF THE POSSIBILITY OF SUCH DAMAGES. 17. Interpretation of Sections 15 and 16. If the disclaimer of warranty and limitation of liability provided above cannot be given local legal effect according to their terms, reviewing courts shall apply local law that most closely approximates an absolute waiver of all civil liability in connection with the Program, unless a warranty or assumption of liability accompanies a copy of the Program in return for a fee. END OF TERMS AND CONDITIONS How to Apply These Terms to Your New Programs If you develop a new program, and you want it to be of the greatest possible use to the public, the best way to achieve this is to make it free software which everyone can redistribute and change under these terms. To do so, attach the following notices to the program. It is safest to attach them to the start of each source file to most effectively state the exclusion of warranty; and each file should have at least the "copyright" line and a pointer to where the full notice is found. Ddosify - Load testing tool for any web system. Copyright (C) 2021 Ddosify This program is free software: you can redistribute it and/or modify it under the terms of the GNU Affero General Public License as published by the Free Software Foundation, either version 3 of the License, or (at your option) any later version. This program is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU Affero General Public License for more details. You should have received a copy of the GNU Affero General Public License along with this program. If not, see . Also add information on how to contact you by electronic and paper mail. If your software can interact with users remotely through a computer network, you should also make sure that it provides a way for users to get its source. For example, if your program is a web application, its interface could display a "Source" link that leads users to an archive of the code. There are many ways you could offer source, and different solutions will be better for different programs; see section 13 for the specific requirements. You should also get your employer (if you work as a programmer) or school, if any, to sign a "copyright disclaimer" for the program, if necessary. For more information on this, and how to apply and follow the GNU AGPL, see . ================================================ FILE: README.md ================================================
Anteon logo dark
Anteon logo light

eBPF-powered Kubernetes Monitoring and Performance Testing

Anteon Kubernetes Monitoring Service Map

anteon latest version  Anteon license Anteon discord server cncf landscape Anteon Guru

Anteon automatically generates Service Map of your K8s cluster without code instrumentation or sidecars. So you can easily find the bottlenecks in your system. Red lines indicate the high latency between services.

Live DemoDocumentationDiscord

## 🐝 What is Anteon? **Anteon** (formerly Ddosify) is an [open-source](https://github.com/getanteon/anteon), eBPF-based **Kubernetes Monitoring** and **Performance Testing** platform. ### 🔎 Kubernetes Monitoring - **Automatic Service Map Creation:** Anteon automatically creates a **service map** of your cluster without code instrumentation or sidecars. So you can easily [find the bottlenecks](https://getanteon.com/docs/kubernetes-monitoring/#finding-bottlenecks) in your system. - **Performance Insights:** It helps you spot issues like services taking too long to respond or slow SQL queries. - **Real-Time Metrics:** The platform tracks and displays live data on your cluster instances CPU, memory, disk, and network usage. - **Ease of Use:** You don't need to change any code, restart services, or add extra components (like sidecars) to get these insights, thanks to the [eBPF based agent (Alaz)](https://github.com/getanteon/alaz). - **Alerts for Anomalies:** If something unusual, like a sudden increase in CPU usage, happens in your Kubernetes (K8s) cluster, Anteon immediately sends alerts to your Slack. - **Seamless Integration with Performance Testing:** Performance testing is natively integrated with Kubernetes monitoring for a unified experience.

Anteon Kubernetes Monitoring Metrics Anteon tracks and displays live data on your cluster instances CPU, memory, disk, and network usage.

### 🔨 Performance Testing - **Multi-Location Based:** Generate load/performance tests from over 25 countries worldwide. - **Easy Scenario Builder:** Create test scenarios easily without writing any code. - **Seamless Integration with Kubernetes Monitoring:** Performance testing is natively integrated with Kubernetes monitoring for a unified experience. - **Postman Integration:** Import tests directly from Postman, making it convenient for those already using Postman for API development and testing.

Anteon Kubernetes Monitoring Metrics Anteon Performance Testing generates load from worldwide with no-code scenario builder.

## 📚 Documentation - [🐝 Anteon Stack](https://getanteon.com/docs/stack/) - [🚀 Getting Started](https://getanteon.com/docs/getting-started/) - [🔎 Kubernetes Monitoring](https://getanteon.com/docs/kubernetes-monitoring/) - [🔨 Performance Testing](https://getanteon.com/docs/performance-testing/) ## ✨ Ask Anteon Guru If you don’t want to get lost in the documentation, you can ask [Anteon Guru](https://gurubase.io/g/anteon) directly. It's an Anteon-focused AI that uses information from the Anteon Website and Anteon GitHub Repository to answer your questions. Anteon Guru ## ℹ️ About This Repository This repository includes the source code for the Anteon Load Engine (Ddosify). You can access Docker Images for the Anteon Engine and Self Hosted on Docker Hub. Since Anteon is a Verified Publisher on Docker Hub, there isn't any pull limits. - [Ddosify documentation](https://github.com/getanteon/anteon/tree/master/ddosify_engine) provides information on the installation, usage, and features of the Anteon Load Engine. - The [Self-Hosted](https://github.com/getanteon/anteon/tree/master/selfhosted) folder contains installation instructions for the Self-Hosted version. - [Anteon eBPF agent (Alaz)](https://github.com/getanteon/alaz) has its own repository. See the [Anteon website](https://getanteon.com/) for more information. ## 🛠️ Contributing See our [Contribution Guide](./CONTRIBUTING.md) and please follow the [Code of Conduct](./CODE_OF_CONDUCT.md) in all your interactions with the project. Thanks goes to these wonderful people! Made with [contrib.rocks](https://contrib.rocks). ### 📨 Communication You can join our [Discord Server](https://discord.com/invite/9KdnrSUZQg) for issues, feature requests, feedbacks or anything else. ### ⚠️ Disclaimer Anteon is created for testing the performance of web applications. Users must be the owner of the target system. Using it for harmful purposes is extremely forbidden. Anteon team & company is not responsible for its’ usages and consequences. ## 📜 License Licensed under the [AGPLv3](LICENSE) ================================================ FILE: SECURITY.md ================================================ # Anteon Security Policy 🐝 We are committed to maintaining the security and integrity of [Anteon](https://github.com/getanteon/anteon), and we appreciate your help in identifying and addressing potential vulnerabilities. This document outlines the process for reporting security issues, as well as our commitment to addressing them in a timely manner. ## Supported Versions We provide security updates for the following versions: | Version | Supported | | ---------- | --------- | | > `0.15.x` | ✅ | ⚠️ Please note that older versions may not receive security updates. We encourage you to use the [latest version](https://github.com/getanteon/anteon/releases) of the software to benefit from the most recent security enhancements. ## Reporting a Vulnerability If you believe you have discovered a security vulnerability, please follow these steps to report it: 1. Do not create a public issue on GitHub. Disclosing security vulnerabilities publicly can put users at risk. 2. Send an email to our security team at security@getanteon.com with a detailed description of the vulnerability, including the steps to reproduce it, and any relevant information about your environment (e.g., OS, Go version, etc.). 3. If possible, provide a minimal code sample or test case that demonstrates the vulnerability. 4. Allow us a reasonable amount of time to investigate and address the issue before publicly disclosing it. We will make every effort to resolve the issue as soon as possible. We keep you informed of our progress in addressing the issue. ## Our Commitment We take security issues very seriously and are committed to working with you to address any vulnerabilities that you report. We will: - Investigate and validate reported vulnerabilities. - Work on a fix or mitigation for the issue. - Provide regular updates on our progress in resolving the issue. - Notify you when the issue has been resolved and provide details on the changes made. - We appreciate your assistance in maintaining the security of Anteon, and we thank you for your responsible disclosure. ================================================ FILE: assets/ddosify.profile ================================================ export TERM=xterm-256color NC='\033[0m' printf "\e[38;5;172m\n" cat< license: AGPL-3.0-only vendor: Ddosify formats: - apk - deb - rpm release: footer: | ## More? 🚀 - Join our [Discord server](https://discord.com/invite/9KdnrSUZQg) - Follow us on [Twitter](https://twitter.com/getanteon) ================================================ FILE: ddosify_engine/Dockerfile ================================================ FROM golang:1.18.1-alpine as builder WORKDIR /app COPY . ./ RUN go mod download RUN CGO_ENABLED=0 GOOS=linux go build -o /app/ddosify main.go FROM alpine:3.15.4 ENV ENV="/root/.ashrc" WORKDIR /root RUN apk --no-cache add ca-certificates COPY --from=builder /app/ddosify /bin/ COPY assets/ddosify.profile /tmp/profile RUN cat /tmp/profile >> "$ENV" ================================================ FILE: ddosify_engine/Dockerfile.dev ================================================ FROM golang:1.18.1 WORKDIR /workspace COPY go.mod ./ COPY go.sum ./ ENV GOPATH /go ENV GOBIN /go/bin ENV LC_ALL=C.UTF-8 ENV LANG=C.UTF-8 ENV SHELL /bin/zsh RUN apt update && apt install -y git zsh vim fzf locales gcc musl-dev curl iputils-ping telnet graphviz bc jq && rm -rf /var/lib/apt/lists/* RUN sh -c "$(curl -fsSL https://raw.github.com/ohmyzsh/ohmyzsh/master/tools/install.sh)" RUN git clone https://github.com/zsh-users/zsh-syntax-highlighting.git ${ZSH_CUSTOM:-~/.oh-my-zsh/custom}/plugins/zsh-syntax-highlighting RUN git clone https://github.com/zsh-users/zsh-autosuggestions ${ZSH_CUSTOM:-~/.oh-my-zsh/custom}/plugins/zsh-autosuggestions RUN curl -sSfL https://raw.githubusercontent.com/golangci/golangci-lint/master/install.sh | sh -s -- -b $(go env GOPATH)/bin v1.45.2 COPY .devcontainer/.zshrc /root/.zshrc RUN go install -v golang.org/x/tools/gopls@v0.8.3 RUN go install -v github.com/rogpeppe/godef@v1.1.2 RUN go install -v github.com/rakyll/gotest@v0.0.6 RUN go install -v github.com/ramya-rao-a/go-outline@1.0.0 RUN go install -v github.com/go-delve/delve/cmd/dlv@v1.8.1 RUN go install -v golang.org/x/perf/cmd/benchstat@v0.0.0-20221222172245-91a04616dc65 RUN go mod download CMD [ "zsh" ] ================================================ FILE: ddosify_engine/Dockerfile.release ================================================ FROM alpine:3.15.4 ENV ENV="/root/.ashrc" WORKDIR /root RUN apk --no-cache add ca-certificates COPY ddosify /bin/ COPY assets/ddosify.profile /tmp/profile RUN cat /tmp/profile >> "$ENV" ================================================ FILE: ddosify_engine/Jenkinsfile ================================================ pipeline { agent { dockerfile { filename '.devcontainer/Dockerfile.dev' } } environment { PROXY_TEST_USERNAME = credentials('proxy-test-username') PROXY_TEST_PASSWORD = credentials('proxy-test-password') } options { disableConcurrentBuilds() } stages { stage('Unit Test') { steps { sh 'go test -coverpkg=./... -coverprofile=coverage.out ./... -timeout 100s -parallel 4' } } stage('Coverage') { steps { sh 'go tool cover -html=coverage.out -o coverage.html' archiveArtifacts '*.html' sh 'echo "Coverage Report: ${BUILD_URL}artifact/coverage.html"' sh '''t=$(go tool cover -func coverage.out | grep total | tail -1 | awk \'{print substr($3, 1, length($3)-1)}\') if [ "${t%.*}" -lt 80 ]; then echo "Coverage failed ${t}/80" exit 1 fi''' } } stage('Main Race Condition') { steps { lock('multi_branch_server') { sh 'go run --race main.go -t https://servdown.com/ -d 1 -n 1500' sh 'go run --race main.go -config config/config_testdata/race_configs/step_assertions_stdout.json' sh 'go run --race main.go -config config/config_testdata/race_configs/step_assertions_stdout_json.json' sh 'go run --race main.go -config config/config_testdata/race_configs/capture_envs.json' sh 'go run --race main.go -config config/config_testdata/race_configs/global_envs.json' sh 'go test -race -run ^TestDynamicVariableRace$ go.ddosify.com/ddosify/core/scenario/scripting/injection' } } } } post { unstable { slackSend(channel: '#jenkins', color: 'danger', message: "${currentBuild.currentResult}: ${currentBuild.fullDisplayName} - ${BUILD_URL}") } failure { slackSend(channel: '#jenkins', color: 'danger', message: "${currentBuild.currentResult}: ${currentBuild.fullDisplayName} - ${BUILD_URL}") } } } ================================================ FILE: ddosify_engine/Jenkinsfile_benchmark ================================================ pipeline { agent { dockerfile { label 'performance-test' filename '.devcontainer/Dockerfile.dev' } } options { disableConcurrentBuilds() } stages { stage('Performance Test') { steps { lock('multi_branch_server_benchmark') { sh 'set -o pipefail && GOCACHE=/tmp/ go test -benchmem -timeout 60m -benchtime=1x -cpuprof=cpu.out -memprof=mem.out -tracef=trace.out -run=^$ -bench ^BenchmarkEngines/$ -count 1 -runN=10 | tee gobench_branch.txt' } } } stage('Performance Test Develop') { when { // Run on only PR allOf { expression { env.CHANGE_ID != null } expression { env.CHANGE_TARGET != null } expression { env.CHANGE_BRANCH != 'develop' } } } steps { lock('multi_branch_server_benchmark') { sh 'git fetch origin develop:develop || git checkout develop && git pull && set -o pipefail && GOCACHE=/tmp/ go test -benchmem -timeout 60m -benchtime=1x -cpuprof=cpu_develop.out -memprof=mem_develop.out -tracef=trace_develop.out -run=^$ -bench ^BenchmarkEngines/$ -count 1 -runN=10 | tee gobench_develop.txt' sh "git checkout ${BRANCH_NAME}" sh "benchstat -alpha 1.01 --sort delta gobench_develop.txt gobench_branch.txt | tee gobench_branch_result.txt" sh "benchstat -alpha 1.01 --sort delta --html gobench_develop.txt gobench_branch.txt > 00_gobench_result.html && echo ${BUILD_URL}artifact/00_gobench_result.html" withCredentials([usernamePassword(credentialsId: 'ddosifyadmin_comment_github_access', passwordVariable: 'GITHUB_TOKEN', usernameVariable: 'GITHUB_USERNAME')]) { sh "./scripts/testing/benchstat.sh ${GITHUB_TOKEN} ${env.CHANGE_ID}" } } } } } post { always { archiveArtifacts artifacts: '*.out', fingerprint: true archiveArtifacts artifacts: 'gobench_*.txt', fingerprint: true archiveArtifacts artifacts: '00_gobench_result.html', fingerprint: true, allowEmptyArchive: true } unstable { slackSend(channel: '#jenkins', color: 'danger', message: "${currentBuild.currentResult}: ${currentBuild.fullDisplayName} - ${BUILD_URL}") } failure { slackSend(channel: '#jenkins', color: 'danger', message: "${currentBuild.currentResult}: ${currentBuild.fullDisplayName} - ${BUILD_URL}") } } } ================================================ FILE: ddosify_engine/README.md ================================================
Anteon logo dark
Anteon logo light

Ddosify: A high-performance load testing tool

go coverage  go report  ddosify license ddosify discord server ddosify docker image

Ddosify - High-performance load testing tool quick start

Table of Contents - [Features](#features) - [Tutorials / Blog Posts](#tutorials--blog-posts) - [Installation](#installation) - [Docker](#docker) - [Docker Extension](#docker-extension) - [Homebrew Tap (macOS and Linux)](#homebrew-tap-macos-and-linux) - [Linux](#linux) - [Redhat (Fedora, CentOS, RHEL, etc.)](#redhat-fedora-centos-rhel-etc) - [Debian (Ubuntu, Linux Mint, etc.)](#debian-ubuntu-linux-mint-etc) - [Alpine](#alpine) - [FreeBSD](#freebsd) - [Windows Executable](#windows-executable) - [Using the convenience script (macOS and Linux)](#using-the-convenience-script-macos-and-linux) - [Go install from source (macOS, FreeBSD, Linux, Windows)](#go-install-from-source-macos-freebsd-linux-windows) - [Quick Start](#quick-start) - [Advanced Usage](#advanced-usage) - [CLI Flags](#cli-flags) - [Load Types](#load-types) - [Linear](#linear) - [Incremental](#incremental) - [Waved](#waved) - [Configuration](#configuration) - [Parameterization (Dynamic Variables)](#parameterization-dynamic-variables) - [Parameterization on URL](#parameterization-on-url) - [Parameterization on Headers](#parameterization-on-headers) - [Parameterization on Payload (Body)](#parameterization-on-payload-body) - [Parameterization on Basic Authentication](#parameterization-on-basic-authentication) - [Parameterization on Config File](#parameterization-on-config-file) - [Environment Variables](#environment-variables) - [Assertion](#assertion) - [Keywords](#keywords) - [Functions](#functions) - [Operators](#operators) - [Assertion Examples](#assertion-examples) - [Success Criteria (Pass / Fail)](#success-criteria-pass--fail) - [Difference Between Success Criteria and Step Assertions](#difference-between-success-criteria-and-step-assertions) - [Keywords](#keywords-1) - [Functions](#functions-1) - [Examples](#examples) - [Correlation](#correlation) - [Capture with json_path](#capture-with-json_path) - [Capture with XPath on XML](#capture-with-xpath-on-xml) - [Capture with XPath on HTML](#capture-with-xpath-on-html) - [Capture with Regular Expressions](#capture-with-regular-expressions) - [Capture Header Value](#capture-header-value) - [Scenario-Scoped Variables](#scenario-scoped-variables) - [Overall Config and Injection](#overall-config-and-injection) - [Test Data Set](#test-data-set) - [Cookies](#cookies) - [Initial / Custom Cookies](#initial--custom-cookies) - [Cookie Capture](#cookie-capture) - [Cookie Assertion](#cookie-assertion) - [Common Issues](#common-issues) - [macOS Security Issue](#macos-security-issue) - [OS Limit - Too Many Open Files](#os-limit---too-many-open-files) - [Contributing](#contributing) - [Communication](#communication) - [More](#more) - [Disclaimer](#disclaimer) - [License](#license)
- [📥 Installation](https://getanteon.com/docs/ddosify/installation/) - [🚀 Quickstart Guide](https://getanteon.com/docs/ddosify/quickstart/) - [⚙️ Configuration](https://getanteon.com/docs/ddosify/configuration/) - [✨ Examples](https://getanteon.com/docs/ddosify/examples/) - ✅ **[Scenario-Based](#config-file)** - Create your flow in a JSON file. Without a line of code! - ✅ **[Different Load Types](#load-types)** - Test your system's limits across different load types. - ✅ **[Parameterization](#parameterization-dynamic-variables)** - Use dynamic variables just like on Postman. - ✅ **[Correlation](#correlation)** - Extract variables from earlier phases and pass them on to the following ones. - ✅ **[Test Data](#test-data-set)** - Import test data from CSV and use it in the scenario. - ✅ **[Assertion](#assertion)** - Verify that the response matches your expectations. - ✅ **[Success Criteria](#success-criteria-pass--fail)** - Set the success criteria for your test. - ✅ **[Cookies](#cookies)** - Pass cookies through steps and set initial cookies if you want. - ✅ **Widely Used Protocols** - Currently supporting _HTTP, HTTPS, HTTP/2_. Other protocols are on the way. ## Tutorials / Blog Posts - [Testing the Performance of User Authentication Flow](https://getanteon.com/blog/testing-the-performance-of-user-authentication-flow) - [Load Testing a Fintech API with CSV Test Data Import](https://getanteon.com/blog/load-testing-a-fintech-exchange-api-with-csv-test-data-import) ## Installation `ddosify` is available via [Docker](https://hub.docker.com/r/ddosify/ddosify), [Docker Extension](https://hub.docker.com/extensions/ddosify/ddosify-docker-extension), [Homebrew Tap](#homebrew-tap-macos-and-linux), and downloadable as pre-compiled binaries from the [releases page](https://github.com/getanteon/anteon/releases/tag/v1.0.6) for macOS, Linux and Windows. For shell auto completions, see [Ddosify Completions](https://github.com/getanteon/anteon/tree/master/ddosify_engine/completions). ### Docker ```bash docker run -it --rm ddosify/ddosify ``` ### Docker Extension Run Ddosify on Docker Desktop with Ddosify Docker extension. More details [here](https://hub.docker.com/extensions/ddosify/ddosify-docker-extension). ### Homebrew Tap (macOS and Linux) ```bash brew install ddosify/tap/ddosify ``` ### Linux - For ARM architectures change `ddosify_amd64` to `ddosify_arm64` or `ddosify_armv6`. - Superuser privilege is required. #### Redhat (Fedora, CentOS, RHEL, etc.) ```bash rpm -i https://github.com/ddosify/ddosify/releases/download/v1.0.6/ddosify_amd64.rpm ``` #### Debian (Ubuntu, Linux Mint, etc.) ```bash wget https://github.com/ddosify/ddosify/releases/download/v1.0.6/ddosify_amd64.deb dpkg -i ddosify_amd64.deb ``` #### Alpine ```bash wget https://github.com/ddosify/ddosify/releases/download/v1.0.6/ddosify_amd64.apk apk add --allow-untrusted ddosify_amd64.apk ``` ### FreeBSD ```bash pkg install ddosify ``` ### Windows Executable - Download zip file for your architecture from the [releases page](https://github.com/ddosify/ddosify/releases/tag/v1.0.6). - For example, download ddosify version `vx.x.x` with amd64 architecture: `ddosify_x.x.x.zip_windows_amd64` - Unzip `ddosify_x.x.x_windows_amd64.zip` - Open Powershell or CMD (Command Prompt) and change directory to unzipped folder: `ddosify_x.x.x_windows_amd64` - Run ddosify: ```bash .\ddosify.exe -t https://getanteon.com ``` ### Using the convenience script (macOS and Linux) - The script requires root or sudo privileges to move ddosify binary to `/usr/local/bin`. - The script attempts to detect your operating system (macOS or Linux) and architecture (arm64, x86, amd64) to download the appropriate binary from the [releases page](https://github.com/getanteon/anteon/tree/master/ddosify_engine/completions). - By default, the script installs the latest version of `ddosify`. - If you have problems, check [common issues](#common-issues). - Required packages: `curl` and `sudo` ```bash curl -sSfL https://raw.githubusercontent.com/getanteon/anteon/master/scripts/install.sh | sh ``` ### Go install from source (macOS, FreeBSD, Linux, Windows) _Minimum supported Go version is 1.18_ ```bash go install -v go.ddosify.com/ddosify@latest ``` ## Quick Start This section aims to show you how to use Ddosify easily without deep dive into its details. 1. ### Simple load test `ddosify -t https://getanteon.com` The above command runs a load test with the default value that is 100 requests in 10 seconds. 2. ### Using some of the features `ddosify -t https://getanteon.com -n 1000 -d 20 -m PUT -T 7 -P http://proxy_server.com:80` Ddosify sends a total of _1000_ _PUT_ requests to *https://getanteon.com* over proxy _http://proxy_server.com:80_ in _20_ seconds with a timeout of _7_ seconds per request. 3. ### Usage for CI/CD pipelines (JSON output) `ddosify -t https://getanteon.com -o stdout-json | jq .avg_duration` Ddosify outputs the result in JSON format. Then `jq` (or any other command-line JSON processor) fetches the `avg_duration`. The rest depends on your CI/CD flow logic. 4. ### Scenario based load test `ddosify -config config_examples/config.json` Ddosify first sends _HTTP/2 POST_ request to *https://getanteon.com/endpoint_1* using basic auth credentials _test_user:12345_ over proxy _http://proxy_host.com:proxy_port_ and with a timeout of _3_ seconds. Once the response is received, HTTPS GET request will be sent to *https://getanteon.com/endpoint_2* along with the payload included in _config_examples/payload.txt_ file with a timeout of 2 seconds. This flow will be repeated _20_ times in _5_ seconds and response will be written to _stdout_. 5. ### Load test with Dynamic Variables (Parameterization) `ddosify -t https://getanteon.com/{{_randomInt}} -d 10 -n 100 -h 'User-Agent: {{_randomUserAgent}}' -b '{"city": "{{_randomCity}}"}'` Ddosify sends a total of _100_ _GET_ requests to *https://getanteon.com/{{_randomInt}}* in _10_ seconds. `{{_randomInt}}` path generates random integers between 1 and 1000 in every request. Dynamic variables can be used in _URL_, _headers_, _payload (body)_ and _basic authentication_. In this example, Ddosify generates a random user agent in the header and a random city in the body. The full list of the dynamic variables can be found in the [docs](https://getanteon.com/docs/performance-testing/dynamic-variables-parametrization/). 6. ### Correlation (Captured Variables) `ddosify -config ddosify_config_correlation.json` Ddosify allows you to specify variables at the global level and use them throughout the scenario, as well as extract variables from previous steps and inject them to the next steps in each iteration individually. You can inject those variables in requests _url_, _headers_ and _payload(body)_. The example config can be found in [correlation-config-example](#Correlation). 7. ### Test Data `ddosify -config ddosify_data_csv.json` Ddosify allows you to load test data from a file, tag specific columns for later use. You can inject those variables in requests _url_, _headers_ and _payload (body)_. The example config can be found in [test-data-example](#test-data-set). ## Advanced Usage You can configure your load test by the CLI options or a config file. Config file supports more features than the CLI. For example, you can't create a scenario-based load test with CLI options. ### CLI Flags ```bash ddosify [FLAG] ``` | Flag | Description | Type | Default | Required | | ----------------------------------------------------------- | ----------------------------------------------------------------------------------------------------------------- | -------- | -------- | -------- | | `-t` | Target website URL. Example: https://getanteon.com | `string` | - | Yes | | `-n` | Total iteration count | `int` | `100` | No | | `-d` | Test duration in seconds. | `int` | `10` | No | | `-m` | Request method. Available methods for HTTP(s) are _GET, POST, PUT, DELETE, HEAD, PATCH, OPTIONS_ | `string` | `GET` | No | | `-b` | The payload of the network packet. AKA body for the HTTP. | `string` | - | No | | `-a` | Basic authentication. Usage: `-a username:password` | `string` | - | No | | `-h` | Headers of the request. You can provide multiple headers with multiple `-h` flag. Usage: `-h 'Accept: text/html'` | `string` | - | No | | `-T` | Timeout of the request in seconds. | `int` | `5` | No | | `-P` | Proxy address as host:port. `-P 'http://user:pass@proxy_host.com:port'` | `string` | - | No | | `-o` | Test result output destination. Supported outputs are [*stdout, stdout-json*] Other output types will be added. | `string` | `stdout` | No | | `-l` | [Type](#load-types) of the load test. Ddosify supports 3 load types. | `string` | `linear` | No | | `--config` | [Config File](#config-file) of the load test. | `string` | - | No | | `--version` | Prints version, git commit, built date (utc), go information and quit | - | - | No | | `--cert_path` | A path to a certificate file (usually called 'cert.pem') | - | - | No | | `--cert_key_path` | A path to a certificate key file (usually called 'key.pem') | - | - | No | | `--debug` | Iterates the scenario once and prints curl-like verbose result. Note that this flag overrides json config. | `bool` | `false` | No | ### Load Types #### Linear ```bash ddosify -t https://getanteon.com -l linear ``` Result: ![linear load](https://raw.githubusercontent.com/getanteon/anteon/master/assets/linear.gif) _Note:_ If the iteration count is too low for the given duration, the test might be finished earlier than you expect. #### Incremental ```bash ddosify -t https://getanteon.com -l incremental ``` Result: ![incremental load](https://raw.githubusercontent.com/getanteon/anteon/master/assets/incremental.gif) #### Waved ```bash ddosify -t https://getanteon.com -l waved ``` Result: ![waved load](https://raw.githubusercontent.com/getanteon/anteon/master/assets/waved.gif) ### Configuration Configuration file lets you use all the capabilities of Ddosify. The features you can use by config file: - Scenario creation - Environment variables - Correlation - Assertions - Cookies - Custom load type creation - Payload from a file - Multipart/form-data payload - Extra connection configuration - HTTP2 support Usage: ```bash ddosify -config ``` There is an example config file at [config_examples/config.json](https://github.com/getanteon/anteon/blob/master/ddosify_engine/config_examples/config.json). This file contains all of the parameters you can use. Details of each parameter; - `iteration_count` (_optional_) This is the equivalent of the `-n` flag. The difference is that if you have multiple steps in your scenario, this value represents the iteration count of the steps. - `load_type` (_optional_) This is the equivalent of the `-l` flag. - `duration` (_optional_) This is the equivalent of the `-d` flag. - `manual_load` (_optional_) If you are looking for creating your own custom load type, you can use this feature. The example below says that Ddosify will run the scenario 5 times, 10 times, and 20 times, respectively along with the provided durations. `iteration_count` and `duration` will be auto-filled by Ddosify according to `manual_load` configuration. In this example, `iteration_count` will be 35 and the `duration` will be 18 seconds. Also `manual_load` overrides `load_type` if you provide both of them. As a result, you don't need to provide these 3 parameters when using `manual_load`. ```json "manual_load": [ {"duration": 5, "count": 5}, {"duration": 6, "count": 10}, {"duration": 7, "count": 20} ] ``` - `proxy` (_optional_) This is the equivalent of the `-P` flag. - `output` (_optional_) This is the equivalent of the `-o` flag. - `engine_mode` (_optional_) Can be one of `distinct-user`, `repeated-user`, or default mode `ddosify`. - `distinct-user` mode simulates a new user for every iteration. - `repeated-user` mode can use pre-used user in subsequent iterations. - `ddosify` mode is default mode of the engine. In this mode engine runs in its max capacity, and does not show user simulation behaviour. - `env` (_optional_) Scenario-scoped global variables. Note that dynamic variables changes every iteration. ```json "env": { "COMPANY_NAME" :"Ddosify", "randomCountry" : "{{_randomCountry}}" } ``` - `data` (_optional_) Config for loading test data from a CSV file. [CSV data](https://github.com/getanteon/anteon/blob/master/ddosify_engine/config/config_testdata/test.csv) used in below config. ```json "data":{ "info": { "path" : "config/config_testdata/test.csv", "delimiter": ";", "vars": { "0":{"tag":"name"}, "1":{"tag":"city"}, "2":{"tag":"team"}, "3":{"tag":"payload", "type":"json"}, "4":{"tag":"age", "type":"int"} }, "allow_quota" : true, "order": "sequential", "skip_first_line" : true, "skip_empty_line" : true } } ``` | Field | Description | Type | Default | Required? | | ----------------- | ----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | -------- | -------- | --------- | | `path` | Local path or remote url for your CSV file | `string` | - | Yes | | `delimiter` | Delimiter for reading CSV | `string` | `,` | No | | `vars` | Tag columns using column index as key, use `type` field if you want to cast a column to a specific type, default is `string`, can be one of the following: `json`, `int`, `float`,`bool`. | `map` | - | Yes | | `allow_quota` | If set to true, a quote may appear in an unquoted field and a non-doubled quote may appear in a quoted field | `bool` | `false` | No | | `order` | Order of reading records from CSV. Can be `random` or `sequential` | `string` | `random` | No | | `skip_first_line` | Skips first line while reading records from CSV. | `bool` | `false` | No | | `skip_empty_line` | Skips empty lines while reading records from CSV. | `bool` | `true` | No | - `success_criterias` (_optional_) Config for pass fail logic for the test. _abort_ and _delay_ fields can be used to adjust the abort behaviour in case of failure. If abort is true for a rule and rules fails at certain point, engine will decide to abort test immediately if delay is 0 or not given. If delay is given, it will wait for delay seconds and reassert the rule. **Example:** Check _90th percentile_ and _fail_count_; ```json { "duration": 10, , "success_criterias": [ { "rule" : "p90(iteration_duration) < 220", "abort" : false }, { "rule" : "fail_count_perc < 0.1", "abort" : true, "delay" : 1 }, { "rule" : "fail_count < 100", "abort" : true, "delay" : 0 } ], "steps": [....] } ``` - `steps` (_required_) This parameter lets you create your scenario. Ddosify runs the provided steps, respectively. For the given example file step id: 2 will be executed immediately after the response of step id: 1 is received. The order of the execution is the same as the order of the steps in the config file. **Details of each parameter for a step;** - `id` (_required_) Each step must have a unique integer id. - `url` (_required_) This is the equivalent of the `-t` flag. - `name` (_optional_) Name of the step. - `method` (_optional_) This is the equivalent of the `-m` flag. - `headers` (_optional_) List of headers with key:value format. - `payload` (_optional_) Body or payload. This is the equivalent of the `-b` flag. _Note:_ If you want to use `x-www-form-urlencoded`, set Content-Type header to `application/x-www-form-urlencoded`. **Example:** send `x-www-form-urlencoded` data; ```json { "headers": { "Content-Type": "application/x-www-form-urlencoded" }, "payload": "key1=value1&key2=value2" } ``` - `payload_file` (_optional_) If you need a long payload, we suggest using this parameter instead of `payload`. - `payload_multipart` (_optional_) Use this for `multipart/form-data` Content-Type. Accepts list of `form-field` objects, structured as below; ```json { "name": [field-name], "value": [field-value|file-path|url], "type": , // Default "text" "src": // Default "local" } ``` **Example:** Sending form name-value pairs; ```json "payload_multipart": [ { "name": "[field-name]", "value": "[field-value]" } ] ``` **Example:** Sending form name-value pairs and a local file; ```json "payload_multipart": [ { "name": "[field-name]", "value": "[field-value]", }, { "name": "[field-name]", "value": "./test.png", "type": "file" } ] ``` **Example:** Sending form name-value pairs and a local file and a remote file; ```json "payload_multipart": [ { "name": "[field-name]", "value": "[field-value]", }, { "name": "[field-name]", "value": "./test.png", "type": "file" }, { "name": "[field-name]", "value": "http://getanteon.com/test.png", "type": "file", "src": "remote" } ] ``` _Note:_ Ddosify adds `Content-Type: multipart/form-data; boundary=[generated-boundary-value]` header to the request when using `payload_multipart`. - `timeout` (_optional_) This is the equivalent of the `-T` flag. - `capture_env` (_optional_) Config for extraction of variables to use them in next steps. **Example:** Capture _NUM_ variable from steps response body; ```json "steps": [ { "id": 1, "url": "http://getanteon.com/endpoint1", "capture_env": { "NUM" :{"from":"body","json_path":"num"}, } }, ] ``` - `assertion` (_optional_) The response from this step will be subject to the assertion rules. If one of the provided rules fails, step is considered as failure. **Example:** Check _status code_ and _content-length_ header values; ```json "steps": [ { "id": 1, "url": "http://getanteon.com/endpoint1", "assertion": [ "equals(status_code,200)", "in(headers.content-length,[2000,3000])" ] }, ] ``` - `sleep` (_optional_) Sleep duration(ms) before executing the next step. Can be an exact duration or a range. **Example:** Sleep 1000ms after step-1; ```json "steps": [ { "id": 1, "url": "http://getanteon.com/endpoint1", "sleep": "1000" }, { "id": 2, "url": "http://getanteon.com/endpoint2", } ] ``` **Example:** Sleep between 300ms-500ms after step-1; ```json "steps": [ { "id": 1, "url": "http://getanteon.com/endpoint1", "sleep": "300-500" }, { "id": 2, "url": "http://getanteon.com/endpoint2", } ] ``` - `auth` (_optional_) Basic authentication. ```json "auth": { "username": "test_user", "password": "12345" } ``` - `others` (_optional_) This parameter accepts dynamic _key: value_ pairs to configure connection details of the protocol in use. ```json "others": { "disable-compression": false, // Default true "h2": true, // Enables HTTP/2. Default false. "disable-redirect": true // Default false } ``` ## Parameterization (Dynamic Variables) Just like the Postman, Ddosify supports parameterization (dynamic variables) on _URL_, _headers_, _payload (body)_ and _basic authentication_. Actually, we support all the random methods Postman supports. If you use `{{$randomVariable}}` on Postman you can use it as `{{_randomVariable}}` on Ddosify. Just change `$` to `_` and you will be fine. To simulate a realistic load test on your system, Ddosify can send every request with dynamic variables. The full list of dynamic variables can be found in the [documentation](https://getanteon.com/docs/performance-testing/dynamic-variables-parametrization/). ### Parameterization on URL Ddosify sends _100_ GET requests in _10_ seconds with random string `key` parameter. This approach can be also used in cache bypass. ```bash ddosify -t https://getanteon.com/?key={{_randomString}} -d 10 -n 100 ``` ### Parameterization on Headers Ddosify sends _100_ GET requests in _10_ seconds with random `Transaction-Type` and `Country` headers. ```bash ddosify -t https://getanteon.com -d 10 -n 100 -h 'Transaction-Type: {{_randomTransactionType}}' -h 'Country: {{_randomCountry}}' ``` ### Parameterization on Payload (Body) Ddosify sends _100_ GET requests in _10_ seconds with random `latitude` and `longitude` values in body. ```bash ddosify -t https://getanteon.com -d 10 -n 100 -b '{"latitude": "{{_randomLatitude}}", "longitude": "{{_randomLongitude}}"}' ``` ### Parameterization on Basic Authentication Ddosify sends _100_ GET requests in _10_ seconds with random `username` and `password` with basic authentication. ```bash ddosify -t https://getanteon.com -d 10 -n 100 -a '{{_randomUserName}}:{{_randomPassword}}' ``` ### Parameterization on Config File Dynamic variables can be used on config file as well. Ddosify sends _100_ GET requests in _10_ seconds with random string `key` parameter in URL and random `User-Key` header. ```bash ddosify -config ddosify_config_dynamic.json ``` ```json { "iteration_count": 100, "load_type": "linear", "duration": 10, "steps": [ { "id": 1, "url": "https://getanteon.com/?key={{_randomString}}", "method": "POST", "headers": { "User-Key": "{{_randomInt}}" } } ] } ``` ### Environment Variables In addition, you can also use operating system environment variables. To access these variables, simply add the `$` prefix followed by the variable name wrapped in double curly braces. The syntax for this is `{{$OS_ENV_VARIABLE}}` within the **config file**. For instance, to use the `USER` environment variable from your operating system, simply input `{{$USER}}`. You can use operating system environment variables in `URL`, `Headers`, `Body (Payload)`, and `Basic Authentication`. Here is an example of using operating system environment variables in the config file. `TARGET_SITE` operating system environment variable is used in `URL` and `USER` environment variable is used in `Headers`. ```bash export TARGET_SITE="https://getanteon.com" ddosify -config ddosify_config_os_env.json ``` ```json { "iteration_count": 100, "load_type": "linear", "duration": 10, "steps": [ { "id": 1, "url": "{{$TARGET_SITE}}", "method": "POST", "headers": { "os-env-user": "{{$USER}}" } } ] } ``` ## Assertion By default, Ddosify marks a step result as successful if it sends the request and receives the response without any network errors. Status code or body type (or content) does not affect the success/failure criteria. However, this may not provide a good test result for your use case, and you may want to create your own success/fail logic. That's where Assertions come in. Ddosify supports assertions on `status code`, `response body`, `response size`, `response time`, `headers`, and `variables`. You can use the `assertion` parameter in the config file to check if the response matches the given condition per step. If the condition is not met, Ddosify will fail the step. Check the [example config](https://github.com/getanteon/anteon/blob/master/ddosify_engine/config_examples/config.json) to see how it looks. As shown in the related table, the first five keywords store different data related to the response. The last keyword, `variables`, stores the current state of environment variables for the step. You can use [Functions](#functions) or [Operators](#operators) to build conditional expressions based on these keywords. You can write multiple assertions for a step. If any assertion fails, the step is marked as failed. If Ddosify can't receive the response for a request, that step is marked as failed without processing the assertions. You will see a **Server Error** as the failure reason in the test result instead of an **Assertion Error**. ### Keywords | Keyword | Description | Usage | | --------------- | ----------------------------- | ------------------ | | `status_code` | Status code | - | | `body` | Response body | - | | `response_size` | Response size in bytes | - | | `response_time` | Response time in ms | - | | `headers` | Response headers | headers.header-key | | `variables` | Global and captured variables | variables.VarName | ### Functions | Function | Parameters | Description | | ---------------- | ----------------------------------------------- | ------------------------------------------------------------------------------- | | `less_than` | ( param `int`, limit `int` ) | checks if param is less than limit | | `greater_than` | ( param `int`, limit `int` ) | checks if param is greater than limit | | `exists` | ( param `any` ) | checks if variable exists | | `equals` | ( param1 `any`, param2 `any` ) | checks if given parameters are equal | | `equals_on_file` | ( param `any`, file_path `string` ) | reads from given file path and checks if it equals to given parameter | | `in` | ( param `any`, array_param `array` ) | checks if expression is in given array | | `contains` | ( param1 `any`, param2 `any` ) | makes substring with param1 inside param2 | | `not` | ( param `bool` ) | returns converse of given param | | `range` | ( param `int`, low `int`,high `int` ) | returns param is in range of [low,high): low is included, high is not included. | | `json_path` | ( json_path `string`) | extracts from response body using given json path | | `xpath` | ( xpath `string` ) | extracts from response body using given xml path | | `html_path` | ( html `string` ) | extracts from response body using given html path | | `regexp` | ( param `any`, regexp `string`, matchNo `int` ) | extracts from given value in the first parameter using given regular expression | ### Operators | Operator | Description | | -------- | ------------ | | `==` | equals | | `!=` | not equals | | `>` | greater than | | `<` | less than | | `!` | not | | `&&` | and | | `\|\|` | or | ### Assertion Examples | Expression | Description | | -------------------------------------------------- | ------------------------------------------------------------------------------- | | `less_than(status_code,201)` | checks if status code is less than 201 | | `equals(status_code,200)` | checks if status code equals to 200 | | `status_code == 200` | same as preceding one | | `not(status_code == 500)` | checks if status code not equals to 500 | | `status_code != 500` | same as preceding one | | `equals(json_path(\"employees.0.name\"),\"Name\")` | checks if json extracted value is equal to "Name" | | `equals(xpath(\"//item/title\"),\"ABC\")` | checks if xml extracted value is equal to "ABC" | | `equals(html_path(\"//body/h1\"),\"ABC\")` | checks if html extracted value is equal to "ABC" | | `equals(variables.x,100)` | checks if `x` variable coming from global or captured variables is equal to 100 | | `equals(variables.x,variables.y)` | checks if variables `x` and `y` are equal to each other | | `equals_on_file(body,\"file.json\")` | reads from file.json and compares response body with read file | | `exists(headers.Content-Type)` | checks if content-type header exists in response headers | | `contains(body,\"xyz\")` | checks if body contains "xyz" in it | | `range(headers.content-length,100,300)` | checks if content-length header is in range [100,300) | | `in(status_code,[200,201])` | checks if status code equal to 200 or 201 | | `(status_code == 200) \|\| (status_code == 201)` | same as preceding one | | `regexp(body,\"[a-z]+_[0-9]+\",0) == \"messi_10\"` | checks if matched result from regex is equal to "messi_10" | ## Success Criteria (Pass / Fail) Ddosify supports success criteria, allowing users to verify the success of their load tests based on response times and failure counts of iterations. With this feature, users can assert the percentile of response times and the failure counts of all iterations in a test. Users can specify the required percentile of response times and failure counts in the configuration file, and the engine will compare the actual response times and failure counts to these values throughout the test continuously. According to the user's configuration, the test can be aborted or continue running until the end. Check the [example config](https://github.com/getanteon/anteon/blob/master/ddosify_engine/config_examples/config.json) to see how the `success_criterias` keyword looks. Note that the functions and operators mentioned in the [Step Assertion](#assertion) section can also be utilized for the Success Criteria keywords listed below. You can see a success criteria example in the [EXAMPLES](https://github.com/getanteon/anteon/blob/master/ddosify_engine/EXAMPLES.md#example-2-success-criteria) file. ## Difference Between Success Criteria and Step Assertions Unlike assertions focused on individual steps, which determine the success or failure of a step according to its response, Success Criteria create an abort/continue logic for the entire test, which is based on the accumulated data from all iterations. ### Keywords | Keyword | Description | Usage | | -------------------- | ------------------------------------- | ----------------------------------------------------------------- | | `fail_count` | Failure count of iterations | Used for aborting when test exceeds certain fail_count | | `iteration_duration` | Response times of iterations in ms | Used for percentile functions | | `fail_count_perc` | Fail count percentage, in range [0,1] | Used for aborting when test exceeds certain fail count percentage | ### Functions | Function | Parameters | Description | | -------- | ------------------- | ------------------------------------------------- | | `p99` | ( arr `int array` ) | 99th percentile, use as `p99(iteration_duration)` | | `p98` | ( arr `int array` ) | 98th percentile, use as `p98(iteration_duration)` | | `p95` | ( arr `int array`) | 95th percentile, use as `p95(iteration_duration)` | | `p90` | ( arr `int array`) | 90th percentile, use as `p90(iteration_duration)` | | `p80` | ( arr `int array`) | 80th percentile, use as `p80(iteration_duration)` | | `min` | ( arr `int array`) | returns minimum element | | `max` | ( arr `int array`) | returns maximum element | | `avg` | ( arr `int array`) | calculates and returns average | ### Examples | Expression | Description | | --------------------------------- | -------------------------------------------- | | `p95(iteration_duration) < 100` | 95th percentile should be less than 100 ms | | `less_than(fail_count,120)` | Total fail count should be less than 120 | | `less_than(fail_count_perc,0.05)` | Fail count percentage should be less than 5% | ## Correlation Ddosify enables you to capture variables from steps using **json_path**, **xpath**, **xpath_html**, or **regular expressions**. Later, in the subsequent steps, you can inject both the captured variables and the scenario-scoped global variables. > **:warning: Points to keep in mind** > > - You must specify **'header_key'** when capturing from header. > - For json_path syntax, please take a look at [gjson syntax](https://github.com/tidwall/gjson/blob/master/SYNTAX.md) doc. > - Regular expression are expected in **'Golang'** style regex. For converting your existing regular expressions, you can use [regex101](https://regex101.com). > - You can extract values from **headers**, **body**, and **cookies**. You can use **debug** parameter to validate your config. ```bash ddosify -config ddosify_config_correlation.json -debug ``` ### Capture with json_path ```json { "steps": [ { "capture_env": { "NUM": { "from": "body", "json_path": "num" }, "NAME": { "from": "body", "json_path": "name" }, "SQUAD": { "from": "body", "json_path": "squad" }, "PLAYERS": { "from": "body", "json_path": "squad.players" }, "MESSI": { "from": "body", "json_path": "squad.players.0" } } } ] } ``` ### Capture with XPath on XML ```json { "steps": [ { "capture_env": { "TITLE": { "from": "body", "xpath": "//item/title" } } } ] } ``` ### Capture with XPath on HTML ```json { "steps": [ { "capture_env": { "TITLE": { "from": "body", "xpath_html": "//body/h1" } } } ] } ``` ### Capture with Regular Expressions ```json { "steps": [ { "capture_env": { "CONTENT_TYPE": { "from": "header", "header_key": "Content-Type", "regexp": { "exp": "application/(\\w)+", "matchNo": 0 } }, "REGEX_MATCH_ENV": { "from": "body", "regexp": { "exp": "[a-z]+_[0-9]+", "matchNo": 1 } } } } ] } ``` ### Capture Header Value ```json { "steps": [ { "capture_env": { "TOKEN": { "from": "header", "header_key": "Authorization" } } } ] } ``` ### Scenario-Scoped Variables ```json { "env": { "TARGET_URL": "http://localhost:8084/hello", "USER_KEY": "ABC", "COMPANY_NAME": "Ddosify", "RANDOM_COUNTRY": "{{_randomCountry}}", "NUMBERS": [22, 33, 10, 52] } } ``` ### Overall Config and Injection On array-like captured variables or environment vars, the **rand( )** function can be utilized. ```json // ddosify_config_correlation.json { "iteration_count": 100, "load_type": "linear", "duration": 10, "steps": [ { "id": 1, "url": "{{TARGET_URL}}", "method": "POST", "headers": { "User-Key": "{{USER_KEY}}", "Rand-Selected-Num": "{{rand(NUMBERS)}}" }, "payload": "{{COMPANY_NAME}}", "capture_env": { "NUM": { "from": "body", "json_path": "num" }, "NAME": { "from": "body", "json_path": "name" }, "SQUAD": { "from": "body", "json_path": "squad" }, "PLAYERS": { "from": "body", "json_path": "squad.players" }, "MESSI": { "from": "body", "json_path": "squad.players.0" }, "TOKEN": { "from": "header", "header_key": "Authorization" }, "CONTENT_TYPE": { "from": "header", "header_key": "Content-Type", "regexp": { "exp": "application/(\\w)+", "matchNo": 0 } } } }, { "id": 2, "url": "{{TARGET_URL}}", "method": "POST", "headers": { "User-Key": "{{USER_KEY}}", "Authorization": "{{TOKEN}}", "Content-Type": "{{CONTENT_TYPE}}" }, "payload_file": "payload.json", "capture_env": { "TITLE": { "from": "body", "xpath": "//item/title" }, "REGEX_MATCH_ENV": { "from": "body", "regexp": { "exp": "[a-z]+_[0-9]+", "matchNo": 1 } } } } ], "env": { "TARGET_URL": "http://localhost:8084/hello", "USER_KEY": "ABC", "COMPANY_NAME": "Ddosify", "RANDOM_COUNTRY": "{{_randomCountry}}", "NUMBERS": [22, 33, 10, 52] } } ``` ```json // payload.json { "boolField": "{{_randomBoolean}}", "numField": "{{NUM}}", "strField": "{{NAME}}", "numArrayField": ["{{NUM}}", 34], "strArrayField": ["{{NAME}}", "hello"], "mixedArrayField": ["{{NUM}}", 34, "{{NAME}}", "{{SQUAD}}"], "{{NAME}}": "messi", "obj": { "numField": "{{NUM}}", "objectField": "{{SQUAD}}", "arrayField": "{{PLAYERS}}" } } ``` ## Test Data Set Ddosify enables you to load test data from **CSV** files. Later, in your scenario, you can inject variables that you tagged. We are using this [CSV data](https://github.com/getanteon/anteon/tree/master/ddosify_engine/config/config_testdata/test.csv) in config below. ```json // config_data_csv.json "data":{ "csv_test": { "path" : "config/config_testdata/test.csv", "delimiter": ";", "vars": { "0":{"tag":"name"}, "1":{"tag":"city"}, "2":{"tag":"team"}, "3":{"tag":"payload", "type":"json"}, "4":{"tag":"age", "type":"int"} }, "allow_quota" : true, "order": "random", "skip_first_line" : true } } ``` You can refer to tagged variables in your request like below. ```json // payload.json { "name": "{{data.csv_test.name}}", "team": "{{data.csv_test.team}}", "city": "{{data.csv_test.city}}", "payload": "{{data.csv_test.payload}}", "age": "{{data.csv_test.age}}" } ``` ## Cookies Ddosify supports cookies in the following engine modes: `distinct-user` and `repeated-user`. Cookies are not supported in the default `ddosify` mode. In `repeated-user` mode, Ddosify uses the same cookie jar for all iterations executed by the same user. It sets cookies returned at the first successful iteration and does not change them afterward. This way, the same cookies are passed through steps in all iterations executed by the same user. In `distinct-user` mode, Ddosify uses a different cookie jar for each iteration, so cookies are passed through steps in one iteration only. You can see a cookie example in the [EXAMPLES](https://github.com/getanteon/anteon/blob/master/ddosify_engine/EXAMPLES.md#example-1-cookie-support) file. ### Initial / Custom Cookies You can set initial/custom cookies for your test scenario using `cookie_jar` field in the config file. You can enable/disable custom cookies with `enabled` key. Check the [example config](https://github.com/getanteon/anteon/blob/master/ddosify_engine/config/config_testdata/config_init_cookies.json). | Key | Description | Example | | ----------- | --------------------------------------------------------------------------------------------------------------- | ----------------------------------------------------------------- | | `name` | The name of the cookie. This field is used to identify the cookie. | `platform` | | `value` | The value of the cookie. This field contains the data that the cookie stores. | `web` | | `domain` | Domain or subdomain that can access the cookie. | `app.getanteon.com` | | `path` | Path within the domain that can access the cookie. | `/` | | `expires` | When the cookie should expire. The date format should be rfc2616. | `Thu, 16 Mar 2023 09:24:02 GMT` | | `max_age` | Number of seconds until the cookie expires. | `5` | | `http_only` | Whether the cookie should only be accessible through HTTP or HTTPS headers, and not through client-side scripts | `true` | | `secure` | Whether the cookie should only be sent over a secure (HTTPS) connection | `false` | | `raw` | The raw format of the cookie. If it is used, the other keys are discarded. | `myCookie=myValue; Expires=Wed, 21 Oct 2026 07:28:00 GMT; Path=/` | ### Cookie Capture You can capture values from cookies from its name just like you do for headers and body and use them in your test scenario. ```json { "iteration_count": 100, "load_type": "linear", "duration": 10, "steps": [ { ... "capture_env": { "TEST" :{"from":"cookies","cookie_name":"test"} } } ] } ``` ### Cookie Assertion You can refer to cookie values as `cookies.cookie_name` while you write assertions for your steps. Following fields are available for cookie assertion: - `name`: Name of the cookie - `domain`: Domain of the cookie - `path`: Path of the cookie - `value`: Value of the cookie - `expires`: Expiration date of the cookie - `maxAge`: Max age of the cookie - `secure`: Secure flag of the cookie - `httpOnly`: Http only flag of the cookie - `rawExpires`: Raw expiration date of the cookie **Examples:** - `cookies.test.expires < time(\"Thu, 01 Jan 1990 00:00:00 GMT\")` is a valid assertion expression. It checks if the cookie named `test` has an expiration date before `Thu, 01 Jan 1990 00:00:00 GMT`. - `cookies.test.path == \"/login\"` is another valid assertion expression. It checks if the cookie named `test` has a path value equal to `/login`. ## Common Issues ### macOS Security Issue ``` "ddosify" can’t be opened because Apple cannot check it for malicious software. ``` - Open `/usr/local/bin` - Right click `ddosify` and select Open - Select Open - Close the opened terminal ### OS Limit - Too Many Open Files If you create large load tests, you may encounter the following errors: ``` Server Error Distribution (Count:Reason): 199 :Get "https://getanteon.com": dial tcp 188.114.96.3:443: socket: too many open files 159 :Get "https://getanteon.com": dial tcp 188.114.97.3:443: socket: too many open files ``` This is because the OS limits the number of open files. You can check the current limit by running `ulimit -n` command. You can increase this limit to 50000 by running the following command on both Linux and macOS. ```bash ulimit -n 50000 ``` But this will only increase the limit for the current session. To increase the limit permanently, you can change the shell configuration file. For example, if you are using bash, you can add the following lines to `~/.bashrc` file. If you are using zsh, you can add the following lines to `~/.zshrc` file. ```bash # For .bashrc echo "ulimit -n 50000" >> ~/.bashrc # For .zshrc echo "ulimit -n 50000" >> ~/.zshrc ``` ## Contributing See our [Contribution Guide](../CONTRIBUTING.md) and please follow the [Code of Conduct](../CODE_OF_CONDUCT.md) in all your interactions with the project. ## Communication You can join our [Discord Server](https://discord.com/invite/9KdnrSUZQg) for issues, feature requests, feedbacks or anything else. ## Disclaimer Ddosify is created for testing the performance of web applications. Users must be the owner of the target system. Using it for harmful purposes is extremely forbidden. Ddosify team & company is not responsible for its’ usages and consequences. ## License Licensed under the [AGPLv3](../LICENSE) ================================================ FILE: ddosify_engine/completions/README.md ================================================ # Shell completions ## Zsh `completions/_ddosify` provides a basic auto-completions. You can apply one of the steps to get an auto-completion successfully. You can locate the file in any directory referenced by `$fpath`. You can use the following command to list directories in `$fpath`. ```bash echo $fpath | tr ' ' '\n' ``` For example, if you are using [oh-my-zsh](https://ohmyz.sh/) you can add it as a plugin after locating file under plugin related directory appeared in `$fpath`. You can create a directory named `ddosify` under `~/.oh-my-zsh/plugins` and copy `_ddosify` file to it. ```bash mkdir -p ~/.oh-my-zsh/plugins/ddosify cp completions/_ddosify ~/.oh-my-zsh/plugins/ddosify ``` Then, you can add `ddosify` to your plugins list in `~/.zshrc` file. ``` # ~/.zshrc plugins=( ... ddosify ) ``` If you don't have an appropriate directory, you can create one and add it to `$fpath`. ``` mkdir -p ${ZDOTDIR:-~}/.zsh_functions echo 'fpath+=${ZDOTDIR:-~}/.zsh_functions' >> ${ZDOTDIR:-~}/.zshrc ``` Then, you can copy `_ddosify` file to the directory you created. ``` cp completions/_ddosify ${ZDOTDIR:-~}/.zsh_functions/_ddosify ``` ================================================ FILE: ddosify_engine/completions/_ddosify ================================================ #compdef ddosify _ddosify typeset -A opt_args _ddosify() { local curcontext="$curcontext" state line local -a opts opts+=( "-t[Target URL.]" "-P[Proxy address as protocol\://username\:password@host\:port. Supported proxies \[http(s), socks\].]" "-T[Request timeout in seconds (default 5).]" "-a[Basic authentication, username\:password.]" "-b[Payload of the network packet (body).]" "-cert_key_path[A path to a certificate key file (usually called 'key.pem').]:filename:_files" "-cert_path[A path to a certificate file (usually called 'cert.pem'.)]:filename:_files" "-config[Json config file path. If a config file is provided, other flag values will be ignored.]:filename:_files" "-d[Test duration in seconds (default 10).]" "-debug[Iterates the scenario once and prints curl-like verbose result.]" "-h[Request Headers. Ex\: -h 'Accept\: text/html' -h 'Content-Type\: application/xml'.]" "-l[Type of the load test \['linear', 'incremental', 'waved'\] (default 'linear').]" "-m[Request Method Type. For Http(s)\:\['GET', 'POST', 'PUT', 'DELETE', 'UPDATE', 'PATCH'\] (default 'GET').]" "-n[Total iteration count (default 100).]" "-o[Output destination (default 'stdout').]" "-version[Prints version, git commit, built date (utc), go information and quit.]" ) _arguments -s -w : $opts && return 0 return 1 } ================================================ FILE: ddosify_engine/config/base.go ================================================ /* * * Ddosify - Load testing tool for any web system. * Copyright (C) 2021 Ddosify (https://ddosify.com) * * This program is free software: you can redistribute it and/or modify * it under the terms of the GNU Affero General Public License as published * by the Free Software Foundation, either version 3 of the License, or * (at your option) any later version. * * This program is distributed in the hope that it will be useful, * but WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * GNU Affero General Public License for more details. * * You should have received a copy of the GNU Affero General Public License * along with this program. If not, see . * */ package config import ( "fmt" "reflect" "go.ddosify.com/ddosify/core/types" ) var AvailableConfigReader = make(map[string]ConfigReader) // ConfigReader is the interface that abstracts different config reader implementations. type ConfigReader interface { Init([]byte) error CreateHammer() (types.Hammer, error) } // NewConfigReader is the factory method of the ConfigReader. func NewConfigReader(config []byte, configType string) (reader ConfigReader, err error) { if val, ok := AvailableConfigReader[configType]; ok { // Create a new object from the service type reader = reflect.New(reflect.TypeOf(val).Elem()).Interface().(ConfigReader) err = reader.Init(config) } else { err = fmt.Errorf("unsupported config reader type: %s", configType) } return } ================================================ FILE: ddosify_engine/config/base_test.go ================================================ /* * * Ddosify - Load testing tool for any web system. * Copyright (C) 2021 Ddosify (https://ddosify.com) * * This program is free software: you can redistribute it and/or modify * it under the terms of the GNU Affero General Public License as published * by the Free Software Foundation, either version 3 of the License, or * (at your option) any later version. * * This program is distributed in the hope that it will be useful, * but WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * GNU Affero General Public License for more details. * * You should have received a copy of the GNU Affero General Public License * along with this program. If not, see . * */ package config import ( "io/ioutil" "os" "reflect" "testing" ) func readConfigFile(path string) []byte { f, _ := os.Open(path) byteValue, _ := ioutil.ReadAll(f) return byteValue } func TestNewConfigReader(t *testing.T) { t.Parallel() configPath := "config_testdata/config.json" reader, err := NewConfigReader(readConfigFile(configPath), ConfigTypeJson) if err != nil { t.Errorf("TestNewConfigReader errored: %v", err) } if reflect.TypeOf(reader) != reflect.TypeOf(&JsonReader{}) { t.Errorf("Expected jsonReader found: %v", reflect.TypeOf(reader)) } } func TestNewConfigReaderInvalidConfigType(t *testing.T) { t.Parallel() configPath := "config_testdata/config.json" _, err := NewConfigReader(readConfigFile(configPath), "invalidConfigType") if err == nil { t.Errorf("TestNewConfigReaderInvalidConfigType errored") } } func TestNewConfigReaderIncorrectJsonFile(t *testing.T) { t.Parallel() configPath := "config_testdata/config_incorrect.json" _, err := NewConfigReader(readConfigFile(configPath), ConfigTypeJson) if err == nil { t.Errorf("TestNewConfigReaderInvalidFilePath errored") } } ================================================ FILE: ddosify_engine/config/config_testdata/benchmark/config_correlation_load_1.json ================================================ { "iteration_count": 100, "engine_mode": "ddosify", "load_type": "waved", "duration": 10, "steps": [ { "id": 1, "url": "{{HTTPBIN}}/json", "name": "JSON", "method": "GET", "others": { "h2": false, "disable-redirect": true, "disable-compression": false }, "headers": { }, "payload": "", "timeout": 3, "capture_env": { "NUM" :{ "from":"body","json_path":"quoteResponse.result.0.askSize"}, "STR" :{ "from":"body","json_path":"quoteResponse.result.0.currency"}, "BOOL": {"from":"body","json_path":"quoteResponse.result.0.cryptoTradeable"}, "FLOAT" : {"from":"body","json_path":"quoteResponse.result.0.epsForward"}, "ALL_RESULT" :{"from":"body","json_path":"quoteResponse.result.0"}, "CONTENT_LENGTH" :{"from":"header", "header_key":"Content-Length"}, "CONTENT_TYPE" :{"from":"header", "header_key":"Content-Type" ,"regexp":{"exp":"application\/(\\w)+","matchNo":0} } } }, { "id": 2, "url": "{{HTTPBIN}}/xml", "name": "XML", "method": "GET", "others": { "h2": false, "disable-redirect": true, "disable-compression": false }, "headers": { "num": "{{NUM}}", "currency": "{{STR}}", "yahoo" : "{{CONTENT_LENGTH}}" }, "payload": "", "timeout": 10 }, { "id": 3, "url": "https://servdown.com", "name": "HTML", "method": "GET", "others": { "h2": false, "disable-redirect": true, "disable-compression": false }, "headers": { "num": "{{NUM}}" }, "payload_file": "config/config_testdata/benchmark/json_payload.json", "timeout": 10 } ], "output": "stdout", "env":{ "HTTPBIN" : "https://httpbin.ddosify.com", "LOCAL" : "http://localhost:8084" }, "debug" : false } ================================================ FILE: ddosify_engine/config/config_testdata/benchmark/config_correlation_load_2.json ================================================ { "iteration_count": 1000, "load_type": "waved", "engine_mode": "ddosify", "duration": 10, "steps": [ { "id": 1, "url": "{{HTTPBIN}}/json", "name": "JSON", "method": "GET", "others": { "h2": false, "disable-redirect": true, "disable-compression": false }, "headers": { }, "payload": "", "timeout": 3, "capture_env": { "NUM" :{ "from":"body","json_path":"quoteResponse.result.0.askSize"}, "STR" :{ "from":"body","json_path":"quoteResponse.result.0.currency"}, "BOOL": {"from":"body","json_path":"quoteResponse.result.0.cryptoTradeable"}, "FLOAT" : {"from":"body","json_path":"quoteResponse.result.0.epsForward"}, "ALL_RESULT" :{"from":"body","json_path":"quoteResponse.result.0"}, "CONTENT_LENGTH" :{"from":"header", "header_key":"Content-Length"}, "CONTENT_TYPE" :{"from":"header", "header_key":"Content-Type" ,"regexp":{"exp":"application\/(\\w)+","matchNo":0} } } }, { "id": 2, "url": "{{HTTPBIN}}/xml", "name": "XML", "method": "GET", "others": { "h2": false, "disable-redirect": true, "disable-compression": false }, "headers": { "num": "{{NUM}}", "currency": "{{STR}}", "yahoo" : "{{CONTENT_LENGTH}}" }, "payload": "", "timeout": 10 }, { "id": 3, "url": "https://servdown.com", "name": "HTML", "method": "GET", "others": { "h2": false, "disable-redirect": true, "disable-compression": false }, "headers": { "num": "{{NUM}}" }, "payload_file": "config/config_testdata/benchmark/json_payload.json", "timeout": 10 } ], "output": "stdout", "env":{ "HTTPBIN" : "https://httpbin.ddosify.com", "LOCAL" : "http://localhost:8084" }, "debug" : false } ================================================ FILE: ddosify_engine/config/config_testdata/benchmark/config_correlation_load_3.json ================================================ { "iteration_count": 5000, "load_type": "waved", "engine_mode": "ddosify", "duration": 10, "steps": [ { "id": 1, "url": "{{HTTPBIN}}/json", "name": "JSON", "method": "GET", "others": { "h2": false, "disable-redirect": true, "disable-compression": false }, "headers": { }, "payload": "", "timeout": 3, "capture_env": { "NUM" :{ "from":"body","json_path":"quoteResponse.result.0.askSize"}, "STR" :{ "from":"body","json_path":"quoteResponse.result.0.currency"}, "BOOL": {"from":"body","json_path":"quoteResponse.result.0.cryptoTradeable"}, "FLOAT" : {"from":"body","json_path":"quoteResponse.result.0.epsForward"}, "ALL_RESULT" :{"from":"body","json_path":"quoteResponse.result.0"}, "CONTENT_LENGTH" :{"from":"header", "header_key":"Content-Length"}, "CONTENT_TYPE" :{"from":"header", "header_key":"Content-Type" ,"regexp":{"exp":"application\/(\\w)+","matchNo":0} } } }, { "id": 2, "url": "{{HTTPBIN}}/xml", "name": "XML", "method": "GET", "others": { "h2": false, "disable-redirect": true, "disable-compression": false }, "headers": { "num": "{{NUM}}", "currency": "{{STR}}", "yahoo" : "{{CONTENT_LENGTH}}" }, "payload": "", "timeout": 10 }, { "id": 3, "url": "https://servdown.com", "name": "HTML", "method": "GET", "others": { "h2": false, "disable-redirect": true, "disable-compression": false }, "headers": { "num": "{{NUM}}" }, "payload_file": "config/config_testdata/benchmark/json_payload.json", "timeout": 10 } ], "output": "stdout", "env":{ "HTTPBIN" : "https://httpbin.ddosify.com", "LOCAL" : "http://localhost:8084" }, "debug" : false } ================================================ FILE: ddosify_engine/config/config_testdata/benchmark/config_correlation_load_4.json ================================================ { "iteration_count": 10000, "load_type": "waved", "engine_mode": "ddosify", "duration": 10, "steps": [ { "id": 1, "url": "{{HTTPBIN}}/json", "name": "JSON", "method": "GET", "others": { "h2": false, "disable-redirect": true, "disable-compression": false }, "headers": { }, "payload": "", "timeout": 3, "capture_env": { "NUM" :{ "from":"body","json_path":"quoteResponse.result.0.askSize"}, "STR" :{ "from":"body","json_path":"quoteResponse.result.0.currency"}, "BOOL": {"from":"body","json_path":"quoteResponse.result.0.cryptoTradeable"}, "FLOAT" : {"from":"body","json_path":"quoteResponse.result.0.epsForward"}, "ALL_RESULT" :{"from":"body","json_path":"quoteResponse.result.0"}, "CONTENT_LENGTH" :{"from":"header", "header_key":"Content-Length"}, "CONTENT_TYPE" :{"from":"header", "header_key":"Content-Type" ,"regexp":{"exp":"application\/(\\w)+","matchNo":0} } } }, { "id": 2, "url": "{{HTTPBIN}}/xml", "name": "XML", "method": "GET", "others": { "h2": false, "disable-redirect": true, "disable-compression": false }, "headers": { "num": "{{NUM}}", "currency": "{{STR}}", "yahoo" : "{{CONTENT_LENGTH}}" }, "payload": "", "timeout": 10 }, { "id": 3, "url": "https://servdown.com", "name": "HTML", "method": "GET", "others": { "h2": false, "disable-redirect": true, "disable-compression": false }, "headers": { "num": "{{NUM}}" }, "payload_file": "config/config_testdata/benchmark/json_payload.json", "timeout": 10 } ], "output": "stdout", "env":{ "HTTPBIN" : "https://httpbin.ddosify.com", "LOCAL" : "http://localhost:8084" }, "debug" : false } ================================================ FILE: ddosify_engine/config/config_testdata/benchmark/config_correlation_load_5.json ================================================ { "iteration_count": 20000, "load_type": "waved", "engine_mode": "ddosify", "duration": 10, "steps": [ { "id": 1, "url": "{{HTTPBIN}}/json", "name": "JSON", "method": "GET", "others": { "h2": false, "disable-redirect": true, "disable-compression": false }, "headers": { }, "payload": "", "timeout": 3, "capture_env": { "NUM" :{ "from":"body","json_path":"quoteResponse.result.0.askSize"}, "STR" :{ "from":"body","json_path":"quoteResponse.result.0.currency"}, "BOOL": {"from":"body","json_path":"quoteResponse.result.0.cryptoTradeable"}, "FLOAT" : {"from":"body","json_path":"quoteResponse.result.0.epsForward"}, "ALL_RESULT" :{"from":"body","json_path":"quoteResponse.result.0"}, "CONTENT_LENGTH" :{"from":"header", "header_key":"Content-Length"}, "CONTENT_TYPE" :{"from":"header", "header_key":"Content-Type" ,"regexp":{"exp":"application\/(\\w)+","matchNo":0} } } }, { "id": 2, "url": "{{HTTPBIN}}/xml", "name": "XML", "method": "GET", "others": { "h2": false, "disable-redirect": true, "disable-compression": false }, "headers": { "num": "{{NUM}}", "currency": "{{STR}}", "yahoo" : "{{CONTENT_LENGTH}}" }, "payload": "", "timeout": 10 }, { "id": 3, "url": "https://servdown.com", "name": "HTML", "method": "GET", "others": { "h2": false, "disable-redirect": true, "disable-compression": false }, "headers": { "num": "{{NUM}}" }, "payload_file": "config/config_testdata/benchmark/json_payload.json", "timeout": 10 } ], "output": "stdout", "env":{ "HTTPBIN" : "https://httpbin.ddosify.com", "LOCAL" : "http://localhost:8084" }, "debug" : false } ================================================ FILE: ddosify_engine/config/config_testdata/benchmark/config_distinct_user.json ================================================ { "iteration_count": 100, "engine_mode": "ddosify", "load_type": "linear", "duration": 10, "steps": [ { "id": 1, "url": "{{HTTPBIN}}/json", "name": "JSON", "method": "GET", "others": { "h2": false, "disable-redirect": true, "disable-compression": false } } ], "output": "stdout", "env":{ "HTTPBIN" : "https://httpbin.ddosify.com" }, "debug" : false, "engine-mode": "distinct-user" } ================================================ FILE: ddosify_engine/config/config_testdata/benchmark/config_multipart_inject_100rps.json ================================================ { "steps": [ { "id": 1, "url": "https://testserver.ddosify.com/upload_image/", "name": "", "method": "POST", "others": { "h2": false, "keep-alive": true, "disable-redirect": true, "disable-compression": false }, "headers": { "Content-Type": "multipart/form-data" }, "timeout": 60, "capture_env": {}, "payload_multipart": [ { "src": "remote", "name": "image", "type": "file", "value": "https://ddosify-backend-storage.s3.amazonaws.com/media/staging/multipart/tVGNdwRxwB-GvQth9mqKITzq28-suX6R3BzvrSH9rWo/random_image.png" }, { "name": "ballot_id", "value": "{{sandikID}}" } ] } ], "output": "stdout", "env":{ "sandikID" : "mamak75yil" }, "duration": 10, "load_type": "linear", "iteration_count": 1000 } ================================================ FILE: ddosify_engine/config/config_testdata/benchmark/config_multipart_inject_10rps.json ================================================ { "steps": [ { "id": 1, "url": "https://testserver.ddosify.com/upload_image/", "name": "", "method": "POST", "others": { "h2": false, "keep-alive": true, "disable-redirect": true, "disable-compression": false }, "headers": { "Content-Type": "multipart/form-data" }, "timeout": 10, "capture_env": {}, "payload_multipart": [ { "src": "remote", "name": "image", "type": "file", "value": "https://ddosify-backend-storage.s3.amazonaws.com/media/staging/multipart/tVGNdwRxwB-GvQth9mqKITzq28-suX6R3BzvrSH9rWo/random_image.png" }, { "name": "ballot_id", "value": "{{_randomInt}}" } ] } ], "output": "stdout", "duration": 10, "load_type": "linear", "iteration_count": 100 } ================================================ FILE: ddosify_engine/config/config_testdata/benchmark/config_multipart_inject_1krps.json ================================================ { "steps": [ { "id": 1, "url": "https://testserver.ddosify.com/upload_image/", "name": "", "method": "POST", "others": { "h2": false, "keep-alive": true, "disable-redirect": true, "disable-compression": false }, "headers": { "Content-Type": "multipart/form-data" }, "timeout": 15, "capture_env": {}, "payload_multipart": [ { "src": "remote", "name": "image", "type": "file", "value": "https://ddosify-backend-storage.s3.amazonaws.com/media/staging/multipart/tVGNdwRxwB-GvQth9mqKITzq28-suX6R3BzvrSH9rWo/random_image.png" }, { "name": "ballot_id", "value": "{{sandikID}}" } ] } ], "output": "stdout", "env":{ "sandikID" : "mamak75yil" }, "duration": 10, "load_type": "linear", "iteration_count": 10000 } ================================================ FILE: ddosify_engine/config/config_testdata/benchmark/config_multipart_inject_200rps.json ================================================ { "steps": [ { "id": 1, "url": "https://testserver.ddosify.com/upload_image/", "name": "", "method": "POST", "others": { "h2": false, "keep-alive": true, "disable-redirect": true, "disable-compression": false }, "headers": { "Content-Type": "multipart/form-data" }, "timeout": 10, "capture_env": {}, "payload_multipart": [ { "src": "remote", "name": "image", "type": "file", "value": "https://ddosify-backend-storage.s3.amazonaws.com/media/staging/multipart/tVGNdwRxwB-GvQth9mqKITzq28-suX6R3BzvrSH9rWo/random_image.png" }, { "name": "ballot_id", "value": "{{sandikID}}" } ] } ], "output": "stdout", "env":{ "sandikID" : "mamak75yil" }, "duration": 10, "load_type": "linear", "iteration_count": 2000 } ================================================ FILE: ddosify_engine/config/config_testdata/benchmark/config_multipart_inject_2krps.json ================================================ { "steps": [ { "id": 1, "url": "https://testserver.ddosify.com/upload_image/", "name": "", "method": "POST", "others": { "h2": false, "keep-alive": true, "disable-redirect": true, "disable-compression": false }, "headers": { "Content-Type": "multipart/form-data" }, "timeout": 30, "capture_env": {}, "payload_multipart": [ { "src": "remote", "name": "image", "type": "file", "value": "https://ddosify-backend-storage.s3.amazonaws.com/media/staging/multipart/tVGNdwRxwB-GvQth9mqKITzq28-suX6R3BzvrSH9rWo/random_image.png" }, { "name": "ballot_id", "value": "{{sandikID}}" } ] } ], "output": "stdout", "env":{ "sandikID" : "mamak75yil" }, "duration": 10, "load_type": "linear", "iteration_count": 20000 } ================================================ FILE: ddosify_engine/config/config_testdata/benchmark/config_multipart_inject_500rps.json ================================================ { "steps": [ { "id": 1, "url": "https://testserver.ddosify.com/upload_image/", "name": "", "method": "POST", "others": { "h2": false, "keep-alive": true, "disable-redirect": true, "disable-compression": false }, "headers": { "Content-Type": "multipart/form-data" }, "timeout": 30, "capture_env": {}, "payload_multipart": [ { "src": "remote", "name": "image", "type": "file", "value": "https://ddosify-backend-storage.s3.amazonaws.com/media/staging/multipart/tVGNdwRxwB-GvQth9mqKITzq28-suX6R3BzvrSH9rWo/random_image.png" }, { "name": "ballot_id", "value": "{{sandikID}}" } ] } ], "output": "stdout", "env":{ "sandikID" : "mamak75yil" }, "duration": 10, "load_type": "linear", "iteration_count": 5000 } ================================================ FILE: ddosify_engine/config/config_testdata/benchmark/config_repeated_user.json ================================================ { "iteration_count": 100, "engine_mode": "ddosify", "load_type": "linear", "duration": 10, "steps": [ { "id": 1, "url": "{{HTTPBIN}}/json", "name": "JSON", "method": "GET", "others": { "h2": false, "disable-redirect": true, "disable-compression": false } } ], "output": "stdout", "env":{ "HTTPBIN" : "https://httpbin.ddosify.com" }, "debug" : false, "engine-mode": "distinct-user" } ================================================ FILE: ddosify_engine/config/config_testdata/benchmark/json_payload.json ================================================ { "boolField" : "{{BOOL}}", "numField" : "{{NUM}}", "strField" : "{{STR}}", "numArrayField" : ["{{NUM}}",34], "strArrayField" : ["{{STR}}","hello"], "mixedArrayField" : ["{{NUM}}",34,"{{FLOAT}}"], "{{STR}}" : "xxxx", "obj" :{ "numField" : "{{CONTENT_LENGTH}}", "objectField" : "{{ALL_RESULT}}" } } ================================================ FILE: ddosify_engine/config/config_testdata/config.json ================================================ { "request_count": 1555, "load_type": "waved", "duration": 21, "steps": [ { "id": 1, "name": "Example Name 1", "url": "https://app.servdown.com/accounts/login/?next=/", "method": "GET", "payload": "payload str", "timeout": 3, "sleep": "1000", "others": { } }, { "id": 2, "name": "Example Name 2", "url": "http://test.com", "method": "PUT", "headers": { "ContenType": "application/xml", "X-ddosify-key": "ajkndalnasd" }, "timeout": 2, "sleep": " 300-500" } ], "output": "stdout", "proxy": "http://proxy_host:80" } ================================================ FILE: ddosify_engine/config/config_testdata/config_auth.json ================================================ { "steps": [ { "id": 1, "url": "https://app.servdown.com/accounts/login/?next=/", "auth": { "type": "basic", "username": "kursat", "password": "12345" } }, { "id": 2, "url": "https://app.servdown.com/accounts/login/?next=/&112f12f12f12f" } ] } ================================================ FILE: ddosify_engine/config/config_testdata/config_capture_environment.json ================================================ { "iteration_count": 100, "load_type": "waved", "duration": 21, "steps": [ { "id": 1, "name": "Example Name 1", "url": "http://localhost:8080/hello", "method": "GET", "capture_env": { "NUM" :{ "from":"body","json_path":"num"}, "X_COOKIE" :{ "from":"cookies","cookie_name":"x"} } }, { "id": 2, "name": "Example Name 2 Json Body", "url": "http://localhost:8080/", "method": "POST", "headers": { "Content-Type": "application/json", "num": "{{NUM}}" }, "capture_env": { "REGEX_MATCH_ENV" :{"from":"body","regexp":{"exp" : "[a-z]+_[0-9]+", "matchNo": 1}} } } ], "debug" : true } ================================================ FILE: ddosify_engine/config/config_testdata/config_data_csv.json ================================================ { "iteration_count": 4, "load_type": "waved", "duration": 1, "steps": [ { "id": 2, "url": "{{LOCAL}}/body", "name": "JSON", "method": "GET", "others": { "h2": false, "disable-redirect": true, "disable-compression": false }, "payload_file": "../config/config_testdata/data_json_payload.json", "timeout": 10 } ], "output": "stdout", "env":{ "HTTPBIN" : "https://httpbin.ddosify.com", "LOCAL" : "http://localhost:8084", "RANDOM_NAMES" : ["kenan","fatih","kursat","semih","sertac"] , "RANDOM_INT" : [52,99,60,33], "RANDOM_BOOL" : [true,true,true,false] }, "data":{ "info": { "path" : "../config/config_testdata/test.csv", "src" : "local", "delimiter": ";", "vars": { "0":{"tag":"name"}, "1":{"tag":"city"}, "2":{"tag":"team"}, "3":{"tag":"payload", "type":"json"}, "4":{"tag":"age", "type":"int"} }, "allow_quota" : true, "order": "random", "skip_first_line" : true } }, "debug" : false } ================================================ FILE: ddosify_engine/config/config_testdata/config_debug_false.json ================================================ { "debug": false, "iteration_count": 1555, "load_type": "waved", "duration": 21, "steps": [ { "id": 1, "name": "Example Name 1", "url": "https://app.servdown.com/accounts/login/?next=/", "method": "GET", "payload": "payload str", "timeout": 3, "sleep": "1000", "others": { } }, { "id": 2, "name": "Example Name 2", "url": "http://test.com", "method": "PUT", "headers": { "ContenType": "application/xml", "X-ddosify-key": "ajkndalnasd" }, "timeout": 2, "sleep": " 300-500" } ], "output": "stdout", "proxy": "http://proxy_host:80" } ================================================ FILE: ddosify_engine/config/config_testdata/config_debug_mode.json ================================================ { "debug": true, "iteration_count": 1555, "load_type": "waved", "duration": 21, "steps": [ { "id": 1, "name": "Example Name 1", "url": "https://app.servdown.com/accounts/login/?next=/", "method": "GET", "payload": "payload str", "timeout": 3, "sleep": "1000", "others": { } }, { "id": 2, "name": "Example Name 2", "url": "http://test.com", "method": "PUT", "headers": { "ContenType": "application/xml", "X-ddosify-key": "ajkndalnasd" }, "timeout": 2, "sleep": " 300-500" } ], "output": "stdout", "proxy": "http://proxy_host:80" } ================================================ FILE: ddosify_engine/config/config_testdata/config_empty.json ================================================ { "steps": [ { "id": 1, "url": "test.com" } ] } ================================================ FILE: ddosify_engine/config/config_testdata/config_global_envs.json ================================================ { "steps": [ { "id": 1, "name": "Example Name 1", "url": "{{LOCAL}}", "method": "GET" }, { "id": 2, "name": "Example Name 2 Json Body", "url": "{{HTTPBIN}}", "method": "GET", "headers": { "Content-Type": "application/json" } } ], "env":{ "HTTPBIN" : "https://httpbin.ddosify.com", "LOCAL" : "http://localhost:8084/hello" } } ================================================ FILE: ddosify_engine/config/config_testdata/config_incorrect.json ================================================ { "request_count": 100, "load_type": "linear", "duration": 10, "steps": [ { "id": 1, "url": "https://app.servdown.com/accounts/login/?next=/", "auth": { "type": "basic", "username": "kursat", "password": "12345" }, "method": "GET", "headers": { "ContenType": "application/xml", "User-Agent": "chrome5" }, "payload": "body yt kanl adnlandlandaln", "timeout": 1, "others": { } }, { "id": 2, "url": "https://app.servdown.com/accounts/login/?next=/&112f12f12f12f", "method": "GET", "headers": { "ContenType": "application/xml", "X-ddosify-key": "ajkndalnasd" }, "payload_file": "config_examples/payload.txt", "timeout": 1, "others": { } }, ], "proxy": "http://proxy_host:80", "output": "stdout" } ================================================ FILE: ddosify_engine/config/config_testdata/config_init_cookies.json ================================================ { "iteration_count": 1555, "load_type": "waved", "duration": 21, "steps": [ { "id": 1, "name": "Example Name 1", "url": "https://app.servdown.com/accounts/login/?next=/", "method": "GET", "payload": "payload str", "timeout": 3, "sleep": "1000", "others": { } } ], "output": "stdout", "engine_mode": "distinct-user", "cookie_jar":{ "enabled" : true, "cookies" :[ { "name": "platform", "value": "web", "domain": "httpbin.ddosify.com", "path": "/", "expires": "Thu, 16 Mar 2023 09:24:02 GMT", "http_only": true, "secure": false } ] } } ================================================ FILE: ddosify_engine/config/config_testdata/config_inject_json.json ================================================ { "iteration_count": 100, "load_type": "waved", "duration": 21, "steps": [ { "id": 1, "name": "Example Name 1", "url": "{{LOCAL}}", "method": "GET", "capture_env": { "NUM" :{ "from":"body","json_path":"num"}, "NAME" :{ "from":"body","json_path":"name"}, "IS_CHAMPION": {"from":"body","json_path":"isChampion"}, "MESSI" : {"from":"body","json_path":"squad.players.0"}, "PLAYERS" :{"from":"body","json_path":"squad.players"}, "SQUAD" :{"from":"body","json_path":"squad"}, "ARGENTINA" :{"from":"header", "header_key":"Argentina"}, "m10" :{"from":"header", "header_key":"Argentina" ,"regexp":{"exp":"[a-z]+_[0-9]+","matchNo":1} } } }, { "id": 2, "name": "Example Name 2 Json Body", "url": "{{LOCAL}}", "method": "POST", "headers": { "Content-Type": "application/json", "num": "{{NUM}}", "bool" : "{{IS_CHAMPION}}" }, "payload_file" : "../config/config_testdata/json_payload.json", "capture_env": { "REGEX_MATCH_ENV" :{"from":"body","json_path":"num"} } } ], "env":{ "HTTPBIN" : "https://httpbin.ddosify.com", "LOCAL" : "http://localhost:8084/hello" }, "debug" : true } ================================================ FILE: ddosify_engine/config/config_testdata/config_inject_json_dynamic.json ================================================ { "iteration_count": 100, "load_type": "waved", "duration": 21, "steps": [ { "id": 1, "name": "Example Name 1", "url": "{{LOCAL}}", "method": "GET", "capture_env": { "NUM" :{ "from":"body","json_path":"num"}, "NAME" :{ "from":"body","json_path":"name"}, "IS_CHAMPION": {"from":"body","json_path":"isChampion"}, "MESSI" : {"from":"body","json_path":"squad.players.0"}, "PLAYERS" :{"from":"body","json_path":"squad.players"}, "SQUAD" :{"from":"body","json_path":"squad"}, "ARGENTINA" :{"from":"header", "header_key":"Argentina"}, "m10" :{"from":"header", "header_key":"Argentina" ,"regexp":{"exp":"[a-z]+_[0-9]+","matchNo":1} } } }, { "id": 2, "name": "Example Name 2 Json Body", "url": "{{LOCAL}}", "method": "POST", "headers": { "Content-Type": "application/json", "num": "{{NUM}}", "bool" : "{{IS_CHAMPION}}" }, "payload_file" : "../config/config_testdata/json_payload_dynamic.json", "capture_env": { "REGEX_MATCH_ENV" :{"from":"body","json_path":"num"} } } ], "env":{ "HTTPBIN" : "https://httpbin.ddosify.com", "LOCAL" : "http://localhost:8084/hello" }, "debug" : true } ================================================ FILE: ddosify_engine/config/config_testdata/config_inject_xml.json ================================================ { "iteration_count": 100, "load_type": "waved", "duration": 21, "steps": [ { "id": 1, "name": "Example Name 1", "url": "{{LOCAL}}", "method": "GET", "payload_file" :"../config/config_testdata/xml_payload.xml" } ], "env":{ "LOCAL" : "http://localhost:8084", "HELLO" : "hello" }, "debug" : true } ================================================ FILE: ddosify_engine/config/config_testdata/config_invalid_capture_env.json ================================================ { "iteration_count": 100, "load_type": "waved", "duration": 21, "steps": [ { "id": 1, "name": "Example Name 1", "url": "{{LOCAL}}", "method": "GET", "capture_env": { "NUM" :{ "from":"body","json_path":"num"} } }, { "id": 2, "name": "Example Name 2 Json Body", "url": "{{HTTPBIN}}", "method": "POST", "headers": { "Content-Type": "application/json", "num": "{{NUM}}" }, "capture_env": { "REGEX_MATCH_ENV" :{"from":"header","regexp":{"exp" : "", "matchNo": 1}} } } ], "debug" : true } ================================================ FILE: ddosify_engine/config/config_testdata/config_invalid_target.json ================================================ { "steps": [ { "id": 1, "url": "_invalid.com" } ] } ================================================ FILE: ddosify_engine/config/config_testdata/config_invalid_user_mode_for_cookies.json ================================================ { "iteration_count": 1555, "load_type": "waved", "duration": 21, "steps": [ { "id": 1, "name": "Example Name 1", "url": "https://app.servdown.com/accounts/login/?next=/", "method": "GET", "payload": "payload str", "timeout": 3, "sleep": "1000", "others": { } } ], "output": "stdout", "cookie_jar":{ "enabled" : true, "cookies" :[ { "name": "platform", "value": "web", "domain": "httpbin.ddosify.com", "path": "/", "expires": "Thu, 16 Mar 2023 09:24:02 GMT", "http_only": true, "secure": false } ] } } ================================================ FILE: ddosify_engine/config/config_testdata/config_iteration_count.json ================================================ { "iteration_count": 1555, "load_type": "waved", "duration": 21, "steps": [ { "id": 1, "name": "Example Name 1", "url": "https://app.servdown.com/accounts/login/?next=/", "method": "GET", "payload": "payload str", "timeout": 3, "sleep": "1000", "others": { } }, { "id": 2, "name": "Example Name 2", "url": "http://test.com", "method": "PUT", "headers": { "ContenType": "application/xml", "X-ddosify-key": "ajkndalnasd" }, "timeout": 2, "sleep": " 300-500" } ], "output": "stdout", "proxy": "http://proxy_host:80" } ================================================ FILE: ddosify_engine/config/config_testdata/config_iteration_count_over_req_count.json ================================================ { "iteration_count": 333, "req_count": 222, "load_type": "waved", "duration": 21, "steps": [ { "id": 1, "name": "Example Name 1", "url": "https://app.servdown.com/accounts/login/?next=/", "method": "GET", "payload": "payload str", "timeout": 3, "sleep": "1000", "others": { } }, { "id": 2, "name": "Example Name 2", "url": "http://test.com", "method": "PUT", "headers": { "ContenType": "application/xml", "X-ddosify-key": "ajkndalnasd" }, "timeout": 2, "sleep": " 300-500" } ], "output": "stdout", "proxy": "http://proxy_host:80" } ================================================ FILE: ddosify_engine/config/config_testdata/config_manual_load.json ================================================ { "manual_load": [ {"duration": 5, "count": 5}, {"duration": 6, "count": 10}, {"duration": 7, "count": 20} ], "steps": [ { "id": 1, "url": "test.com" } ] } ================================================ FILE: ddosify_engine/config/config_testdata/config_manual_load_override.json ================================================ { "requests_count": 100, "duration": 22, "manual_load": [ {"duration": 5, "count": 5}, {"duration": 6, "count": 10}, {"duration": 7, "count": 20} ], "steps": [ { "id": 1, "url": "test.com" } ] } ================================================ FILE: ddosify_engine/config/config_testdata/config_multipart_err.json ================================================ { "steps": [ { "id": 1, "url": "https://app.servdown.com/accounts/login/?next=/", "method": "GET", "payload_multipart": [ { "name": "example-name-5", "value": "https://uplo333ad.wikimedia.org/wikipedia/commons/b/bd/Test.svg", "type": "file", "src": "remote" } ] } ] } ================================================ FILE: ddosify_engine/config/config_testdata/config_multipart_payload.json ================================================ { "steps": [ { "id": 1, "url": "https://app.servdown.com/accounts/login/?next=/", "method": "GET", "payload_multipart": [ { "name": "example-name-1", "value": "config_testdata/test_img.svg", "type": "file" }, { "name": "example-name-2", "value": "https://upload.wikimedia.org/wikipedia/commons/b/bd/Test.svg", "type": "file", "src": "remote" }, { "name": "example-name-3", "value": "text-field-value" }, { "name": "example-name-4", "value": "123123", "type": "text" } ] } ] } ================================================ FILE: ddosify_engine/config/config_testdata/config_payload.json ================================================ { "steps": [ { "id": 1, "url": "https://app.servdown.com/accounts/login/?next=/", "method": "GET", "payload": "payload from string" }, { "id": 2, "url": "https://app.servdown.com/accounts/login/?next=/&112f12f12f12f", "payload_file": "config_testdata/payload.txt" } ] } ================================================ FILE: ddosify_engine/config/config_testdata/config_protocol.json ================================================ { "steps": [ { "id": 1, "url": "https://app.servdown.com/accounts/login/?next=/", "protocol": "http" }, { "id": 2, "url": "http://app.servdown.com/accounts/login/?next=/&112f12f12f12f" }, { "id": 3, "url": "app.servdown.com/accounts/login/?next=/&112f12f12f12f" }, { "id": 4, "url": "app.servdown.com/accounts/login/?next=/&112f12f12f12f", "protocol": "http" } ] } ================================================ FILE: ddosify_engine/config/config_testdata/config_test_assertion_fail.json ================================================ { "iteration_count": 100, "load_type": "linear", "duration": 10, "debug" : false, "success_criterias": [ { "rule" : "false", "abort" : true, "delay" : 1 } ], "steps": [ { "id": 1, "url": "https://httpbin.ddosify.com/json2", "name": "JSON", "method": "GET", "others": { "h2": false, "keep-alive": true, "disable-redirect": true, "disable-compression": false }, "timeout": 2 } ] } ================================================ FILE: ddosify_engine/config/config_testdata/data_json_payload.json ================================================ { "name" : "{{data.info.name}}", "team" : "{{data.info.team}}", "city" : "{{data.info.city}}", "payload" : "{{rand(data.info.payload)}}", "age" : "{{data.info.age}}" } ================================================ FILE: ddosify_engine/config/config_testdata/json_payload.json ================================================ { "boolField" : "{{IS_CHAMPION}}", "numField" : "{{NUM}}", "strField" : "{{NAME}}", "numArrayField" : ["{{NUM}}",34], "strArrayField" : ["{{NAME}}","hello"], "mixedArrayField" : ["{{NUM}}",34,"{{NAME}}","{{SQUAD}}"], "{{NAME}}" : "xxxx", "obj" :{ "numField" : "{{NUM}}", "objectField" : "{{SQUAD}}", "arrayField" : "{{PLAYERS}}" } } ================================================ FILE: ddosify_engine/config/config_testdata/json_payload_dynamic.json ================================================ { "name" : "{{_randomString}}", "city" : "{{_randomCity}}", "age" : "{{_randomInt}}" } ================================================ FILE: ddosify_engine/config/config_testdata/payload.txt ================================================ Payloaf from file. ================================================ FILE: ddosify_engine/config/config_testdata/race_configs/capture_envs.json ================================================ { "iteration_count": 10, "duration": 2, "steps": [ { "id": 1, "name": "Example Name 2 Json Body", "url": "{{HTTPBIN}}/json", "method": "GET", "headers": { "Content-Type": "application/json" }, "capture_env": { "NUM" :{ "from":"body","json_path":"quoteResponse.result.0.askSize"} } }, { "id": 2, "name": "Example Name 1", "url": "{{LOCAL}}", "method": "GET" } ], "env":{ "HTTPBIN" : "https://httpbin.ddosify.com", "LOCAL" : "http://localhost:8084/hello" } } ================================================ FILE: ddosify_engine/config/config_testdata/race_configs/global_envs.json ================================================ { "iteration_count": 10, "duration": 2, "steps": [ { "id": 1, "name": "Example Name 1", "url": "{{LOCAL}}", "method": "GET" }, { "id": 2, "name": "Example Name 2 Json Body", "url": "{{HTTPBIN}}", "method": "GET", "headers": { "Content-Type": "application/json" } } ], "env":{ "HTTPBIN" : "https://httpbin.ddosify.com", "LOCAL" : "http://localhost:8084/hello" } } ================================================ FILE: ddosify_engine/config/config_testdata/race_configs/step_assertions_stdout.json ================================================ { "debug": false, "steps": [ { "id": 1, "url": "https://testserver.ddosify.com/exchange/", "name": "", "method": "GET", "others": { "h2": false, "keep-alive": true, "disable-redirect": true, "disable-compression": false }, "timeout": 10, "capture_env": {}, "assertion": [ "equals(status_code,203)", "contains(body,\"afssafs\")" ] }, { "id": 2, "url": "https://testserver.ddosify.com/exchange/", "name": "", "method": "GET", "others": { "h2": false, "keep-alive": true, "disable-redirect": true, "disable-compression": false }, "timeout": 10, "capture_env": {}, "assertion": [ "equals(status_code,401)" ] }, { "id": 3, "url": "https://teasgsagasgsastserver.ddosify.com/exchange/", "name": "", "method": "GET", "others": { "h2": false, "keep-alive": true, "disable-redirect": true, "disable-compression": false }, "timeout": 10, "capture_env": {}, "assertion": [ "equals(status_code,401)" ] } ], "iteration_count": 10, "duration": 2, "output": "stdout" } ================================================ FILE: ddosify_engine/config/config_testdata/race_configs/step_assertions_stdout_json.json ================================================ { "debug": false, "steps": [ { "id": 1, "url": "https://testserver.ddosify.com/exchange/", "name": "", "method": "GET", "others": { "h2": false, "keep-alive": true, "disable-redirect": true, "disable-compression": false }, "timeout": 10, "capture_env": {}, "assertion": [ "equals(status_code,203)", "contains(body,\"afssafs\")" ] }, { "id": 2, "url": "https://testserver.ddosify.com/exchange/", "name": "", "method": "GET", "others": { "h2": false, "keep-alive": true, "disable-redirect": true, "disable-compression": false }, "timeout": 10, "capture_env": {}, "assertion": [ "equals(status_code,401)" ] }, { "id": 3, "url": "https://teasgsagasgsastserver.ddosify.com/exchange/", "name": "", "method": "GET", "others": { "h2": false, "keep-alive": true, "disable-redirect": true, "disable-compression": false }, "timeout": 10, "capture_env": {}, "assertion": [ "equals(status_code,401)" ] } ], "iteration_count": 10, "duration": 2, "output": "stdout-json" } ================================================ FILE: ddosify_engine/config/config_testdata/test.csv ================================================ Username;City;Team;Payload;Age;Percent;BoolField;;; Kenan;Tokat;Galatasaray;{"data":{"profile":{"name":"Kenan"}}};25;22.3;true;;; Fatih;Bolu;Galatasaray;[5,6,7];29;44.3;false;;; Kursat;Samsun;Besiktas;{"a":"b"};28;12.54;True;;; Semih;Duzce;Besiktas;{"a":"b"};27;663.67;False;;; ;;;;;;;;; ;;;;;;;;; ================================================ FILE: ddosify_engine/config/config_testdata/xml_payload.xml ================================================ {{HELLO}} ================================================ FILE: ddosify_engine/config/json.go ================================================ /* * * Ddosify - Load testing tool for any web system. * Copyright (C) 2021 Ddosify (https://ddosify.com) * * This program is free software: you can redistribute it and/or modify * it under the terms of the GNU Affero General Public License as published * by the Free Software Foundation, either version 3 of the License, or * (at your option) any later version. * * This program is distributed in the hope that it will be useful, * but WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * GNU Affero General Public License for more details. * * You should have received a copy of the GNU Affero General Public License * along with this program. If not, see . * */ package config import ( "bytes" "encoding/json" "fmt" "io" "io/ioutil" "mime/multipart" "net/http" "net/url" "os" "path" "path/filepath" "strings" "unsafe" "go.ddosify.com/ddosify/core/proxy" "go.ddosify.com/ddosify/core/types" ) const ConfigTypeJson = "jsonReader" func init() { AvailableConfigReader[ConfigTypeJson] = &JsonReader{} } type timeRunCount []struct { Duration int `json:"duration"` Count int `json:"count"` } type auth struct { Type string `json:"type"` Username string `json:"username"` Password string `json:"password"` } type multipartFormData struct { Name string `json:"name"` Value string `json:"value"` Type string `json:"type"` Src string `json:"src"` } type RegexCaptureConf struct { Exp *string `json:"exp"` No int `json:"matchNo"` } type capturePath struct { JsonPath *string `json:"json_path"` XPath *string `json:"xpath"` XpathHtml *string `json:"xpath_html"` RegExp *RegexCaptureConf `json:"regexp"` From string `json:"from"` // body,header,cookie CookieName *string `json:"cookie_name"` HeaderKey *string `json:"header_key"` // header key } type step struct { Id uint16 `json:"id"` Name string `json:"name"` Url string `json:"url"` Auth auth `json:"auth"` Method string `json:"method"` Headers map[string]string `json:"headers"` Payload string `json:"payload"` PayloadFile string `json:"payload_file"` PayloadMultipart []multipartFormData `json:"payload_multipart"` Timeout int `json:"timeout"` Sleep string `json:"sleep"` Others map[string]interface{} `json:"others"` CertPath string `json:"cert_path"` CertKeyPath string `json:"cert_key_path"` CaptureEnv map[string]capturePath `json:"capture_env"` Assertions []string `json:"assertion"` } func (s *step) UnmarshalJSON(data []byte) error { type stepAlias step defaultFields := &stepAlias{ Method: types.DefaultMethod, Timeout: types.DefaultTimeout, } err := json.Unmarshal(data, defaultFields) if err != nil { return err } *s = step(*defaultFields) return nil } type Tag struct { Tag string `json:"tag"` Type string `json:"type"` } func (t *Tag) UnmarshalJSON(data []byte) error { // default values t.Type = "string" type tempTag Tag return json.Unmarshal(data, (*tempTag)(t)) } type CsvConf struct { Path string `json:"path"` Delimiter string `json:"delimiter"` SkipFirstLine bool `json:"skip_first_line"` Vars map[string]Tag `json:"vars"` // "0":"name", "1":"city","2":"team" SkipEmptyLine bool `json:"skip_empty_line"` AllowQuota bool `json:"allow_quota"` Order string `json:"order"` } func (c *CsvConf) UnmarshalJSON(data []byte) error { // default values c.SkipEmptyLine = true c.SkipFirstLine = false c.AllowQuota = false c.Delimiter = "," c.Order = "random" type tempCsv CsvConf return json.Unmarshal(data, (*tempCsv)(c)) } type JsonReader struct { ReqCount *int `json:"request_count"` IterCount *int `json:"iteration_count"` LoadType string `json:"load_type"` Duration int `json:"duration"` Assertions []TestAssertion `json:"success_criterias"` TimeRunCount timeRunCount `json:"manual_load"` Steps []step `json:"steps"` Output string `json:"output"` Proxy string `json:"proxy"` Envs map[string]interface{} `json:"env"` Data map[string]CsvConf `json:"data"` Debug bool `json:"debug"` SamplingRate *int `json:"sampling_rate"` EngineMode string `json:"engine_mode"` Cookies CookieConf `json:"cookie_jar"` } type CookieConf struct { Cookies []CustomCookie `json:"cookies"` Enabled bool `json:"enabled"` } type CustomCookie struct { Name string `json:"name"` Value string `json:"value"` Domain string `json:"domain"` Path string `json:"path"` Expires string `json:"expires"` MaxAge int `json:"max_age"` HttpOnly bool `json:"http_only"` Secure bool `json:"secure"` Raw string `json:"raw"` } type TestAssertion struct { Rule string `json:"rule"` Abort bool `json:"abort"` Delay int `json:"delay"` } func (j *JsonReader) UnmarshalJSON(data []byte) error { type jsonReaderAlias JsonReader defaultFields := &jsonReaderAlias{ LoadType: types.DefaultLoadType, Duration: types.DefaultDuration, Output: types.DefaultOutputType, EngineMode: types.EngineModeDdosify, } err := json.Unmarshal(data, defaultFields) if err != nil { return err } *j = JsonReader(*defaultFields) return nil } func (j *JsonReader) Init(jsonByte []byte) (err error) { if !json.Valid(jsonByte) { err = fmt.Errorf("provided json is invalid") return } err = json.Unmarshal(jsonByte, &j) if err != nil { return } return } func (j *JsonReader) CreateHammer() (h types.Hammer, err error) { // Scenario s := types.Scenario{ Envs: j.Envs, } var si types.ScenarioStep for _, step := range j.Steps { si, err = stepToScenarioStep(step) if err != nil { return } s.Steps = append(s.Steps, si) } // Proxy var proxyURL *url.URL if j.Proxy != "" { proxyURL, err = url.Parse(j.Proxy) if err != nil { return } } p := proxy.Proxy{ Strategy: proxy.ProxyTypeSingle, Addr: proxyURL, } // for backwards compatibility var iterationCount int if j.IterCount != nil { iterationCount = *j.IterCount } else if j.ReqCount != nil { iterationCount = *j.ReqCount } else { iterationCount = types.DefaultIterCount } j.IterCount = &iterationCount // TimeRunCount if len(j.TimeRunCount) > 0 { *j.IterCount, j.Duration = 0, 0 for _, t := range j.TimeRunCount { *j.IterCount += t.Count j.Duration += t.Duration } } var samplingRate int if j.SamplingRate != nil { samplingRate = *j.SamplingRate } else { samplingRate = types.DefaultSamplingCount } testDataConf := make(map[string]types.CsvConf) for key, val := range j.Data { vars := make(map[string]types.Tag) for k, v := range val.Vars { vars[k] = types.Tag{ Tag: v.Tag, Type: v.Type, } } testDataConf[key] = types.CsvConf{ Path: val.Path, Delimiter: val.Delimiter, SkipFirstLine: val.SkipFirstLine, Vars: vars, SkipEmptyLine: val.SkipEmptyLine, AllowQuota: val.AllowQuota, Order: val.Order, } } if j.Cookies.Enabled && j.EngineMode == types.EngineModeDdosify { return h, fmt.Errorf("cookies are not supported in ddosify engine mode, please use distinct-user or repeated-user mode") } var testAssertions map[string]types.TestAssertionOpt if len(j.Assertions) > 0 { testAssertions = make(map[string]types.TestAssertionOpt, 0) } for _, as := range j.Assertions { testAssertions[as.Rule] = types.TestAssertionOpt{ Abort: as.Abort, Delay: as.Delay, } } // Hammer h = types.Hammer{ IterationCount: *j.IterCount, LoadType: strings.ToLower(j.LoadType), TestDuration: j.Duration, TimeRunCountMap: types.TimeRunCount(j.TimeRunCount), Scenario: s, Proxy: p, ReportDestination: j.Output, Debug: j.Debug, SamplingRate: samplingRate, EngineMode: j.EngineMode, TestDataConf: testDataConf, Cookies: *(*[]types.CustomCookie)(unsafe.Pointer(&j.Cookies.Cookies)), CookiesEnabled: j.Cookies.Enabled, Assertions: testAssertions, SingleMode: types.DefaultSingleMode, } return } func stepToScenarioStep(s step) (types.ScenarioStep, error) { var payload string var err error if len(s.PayloadMultipart) > 0 { if s.Headers == nil { s.Headers = make(map[string]string) } payload, s.Headers["Content-Type"], err = prepareMultipartPayload(s.PayloadMultipart) if err != nil { return types.ScenarioStep{}, err } } else if s.PayloadFile != "" { var pUrl *url.URL if pUrl, err = url.ParseRequestURI(s.PayloadFile); err == nil && pUrl.IsAbs() { // url payload, err = preparePayloadFile(s.PayloadFile) if err != nil { return types.ScenarioStep{}, err } } else if _, err = os.Stat(s.PayloadFile); err == nil { // local file path buf, err := ioutil.ReadFile(s.PayloadFile) if err != nil { return types.ScenarioStep{}, err } payload = string(buf) } else { return types.ScenarioStep{}, fmt.Errorf("payload file %s not found", s.PayloadFile) } } else { payload = s.Payload } // Set default Auth type if not set if s.Auth != (auth{}) && s.Auth.Type == "" { s.Auth.Type = types.AuthHttpBasic } err = types.IsTargetValid(s.Url) if err != nil { return types.ScenarioStep{}, err } var capturedEnvs []types.EnvCaptureConf for name, path := range s.CaptureEnv { capConf := types.EnvCaptureConf{ JsonPath: path.JsonPath, Xpath: path.XPath, XpathHtml: path.XpathHtml, Name: name, From: types.SourceType(path.From), Key: path.HeaderKey, CookieName: path.CookieName, } if path.RegExp != nil { capConf.RegExp = &types.RegexCaptureConf{ Exp: path.RegExp.Exp, No: path.RegExp.No, } } capturedEnvs = append(capturedEnvs, capConf) } item := types.ScenarioStep{ ID: s.Id, Name: s.Name, URL: s.Url, Auth: types.Auth(s.Auth), Method: strings.ToUpper(s.Method), Headers: s.Headers, Payload: payload, Timeout: s.Timeout, Sleep: strings.ReplaceAll(s.Sleep, " ", ""), Custom: s.Others, EnvsToCapture: capturedEnvs, Assertions: s.Assertions, } if s.CertPath != "" && s.CertKeyPath != "" { cert, pool, err := types.ParseTLS(s.CertPath, s.CertKeyPath) if err != nil { return item, err } item.Cert = cert item.CertPool = pool } return item, nil } func prepareMultipartPayload(parts []multipartFormData) (body string, contentType string, err error) { byteBody := &bytes.Buffer{} writer := multipart.NewWriter(byteBody) for _, part := range parts { var multipartError RemoteMultipartError if strings.EqualFold(part.Type, "file") { if strings.EqualFold(part.Src, "remote") { response, err := http.Get(part.Value) if err != nil { multipartError.wrappedErr = err multipartError.msg = "Error while getting remote file" return "", "", multipartError } defer response.Body.Close() u, _ := url.Parse(part.Value) formPart, err := writer.CreateFormFile(part.Name, path.Base(u.Path)) if err != nil { multipartError.wrappedErr = err multipartError.msg = "Error while creating form file" return "", "", err } if !(response.StatusCode >= 200 && response.StatusCode <= 299) { multipartError.wrappedErr = fmt.Errorf("Multipart: request to remote url (%s) failed. Status code: %d", part.Value, response.StatusCode) multipartError.msg = "Error while getting remote multipart file" return "", "", multipartError } _, err = io.Copy(formPart, response.Body) if err != nil { multipartError.wrappedErr = err multipartError.msg = "Error while copying response body" return "", "", multipartError } } else { file, err := os.Open(part.Value) if err != nil { return "", "", err } defer file.Close() formPart, err := writer.CreateFormFile(part.Name, filepath.Base(file.Name())) if err != nil { return "", "", err } _, err = io.Copy(formPart, file) if err != nil { return "", "", err } } } else { // If we have to specify Content-Type in Content-Disposition, we should use writer.CreatePart directly. err = writer.WriteField(part.Name, part.Value) if err != nil { return "", "", err } } } writer.Close() return byteBody.String(), writer.FormDataContentType(), err } func preparePayloadFile(url string) (body string, err error) { req, err := http.NewRequest(http.MethodGet, url, nil) if err != nil { return "", err } resp, err := http.DefaultClient.Do(req) if err != nil { return "", err } if !(resp.StatusCode >= 200 && resp.StatusCode <= 299) { return "", fmt.Errorf("Payload File: request to remote url (%s) failed. Status Code: %d", url, resp.StatusCode) } defer resp.Body.Close() by, _ := io.ReadAll(resp.Body) return string(by), nil } type RemoteMultipartError struct { // UnWrappable msg string wrappedErr error } func (nf RemoteMultipartError) Error() string { if nf.wrappedErr != nil { return fmt.Sprintf("%s, %s", nf.msg, nf.wrappedErr.Error()) } return nf.msg } func (nf RemoteMultipartError) Unwrap() error { return nf.wrappedErr } ================================================ FILE: ddosify_engine/config/json_test.go ================================================ /* * * Ddosify - Load testing tool for any web system. * Copyright (C) 2021 Ddosify (https://ddosify.com) * * This program is free software: you can redistribute it and/or modify * it under the terms of the GNU Affero General Public License as published * by the Free Software Foundation, either version 3 of the License, or * (at your option) any later version. * * This program is distributed in the hope that it will be useful, * but WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * GNU Affero General Public License for more details. * * You should have received a copy of the GNU Affero General Public License * along with this program. If not, see . * */ package config import ( "errors" "fmt" "io" "net/http" "net/url" "os" "reflect" "regexp" "strings" "testing" "go.ddosify.com/ddosify/core/proxy" "go.ddosify.com/ddosify/core/report" "go.ddosify.com/ddosify/core/types" ) func TestCreateHammerDefaultValues(t *testing.T) { t.Parallel() jsonReader, _ := NewConfigReader(readConfigFile("config_testdata/config_empty.json"), ConfigTypeJson) expectedHammer := types.Hammer{ IterationCount: types.DefaultIterCount, LoadType: types.DefaultLoadType, TestDuration: types.DefaultDuration, ReportDestination: types.DefaultOutputType, Scenario: types.Scenario{ Steps: []types.ScenarioStep{{ ID: 1, URL: "test.com", Method: types.DefaultMethod, Timeout: types.DefaultTimeout, }}, }, Proxy: proxy.Proxy{ Strategy: proxy.ProxyTypeSingle, }, SamplingRate: types.DefaultSamplingCount, EngineMode: types.EngineModeDdosify, TestDataConf: make(map[string]types.CsvConf), SingleMode: true, } h, err := jsonReader.CreateHammer() if err != nil { t.Errorf("TestCreateHammerDefaultValues error occurred: %v", err) } if !reflect.DeepEqual(expectedHammer, h) { t.Errorf("\nExpected: %#v, \nFound: %#v", expectedHammer, h) } } func TestCreateHammer(t *testing.T) { t.Parallel() jsonReader, _ := NewConfigReader(readConfigFile("config_testdata/config.json"), ConfigTypeJson) addr, _ := url.Parse("http://proxy_host:80") expectedHammer := types.Hammer{ IterationCount: 1555, LoadType: types.LoadTypeWaved, TestDuration: 21, ReportDestination: report.OutputTypeStdout, Scenario: types.Scenario{ Steps: []types.ScenarioStep{ { ID: 1, Name: "Example Name 1", URL: "https://app.servdown.com/accounts/login/?next=/", Method: http.MethodGet, Timeout: 3, Sleep: "1000", Payload: "payload str", Custom: map[string]interface{}{}, }, { ID: 2, Name: "Example Name 2", URL: "http://test.com", Method: http.MethodPut, Timeout: 2, Sleep: "300-500", Headers: map[string]string{ "ContenType": "application/xml", "X-ddosify-key": "ajkndalnasd", }, }, }, }, Proxy: proxy.Proxy{ Strategy: "single", Addr: addr, }, SamplingRate: types.DefaultSamplingCount, EngineMode: types.EngineModeDdosify, TestDataConf: make(map[string]types.CsvConf), SingleMode: true, } h, err := jsonReader.CreateHammer() if err != nil { t.Errorf("TestCreateHammer error occurred: %v", err) } if !reflect.DeepEqual(expectedHammer, h) { t.Errorf("\nExpected: %v,\n Found: %v", expectedHammer, h) } } func TestCreateHammerWithIterationCountInsteadOfReqCount(t *testing.T) { t.Parallel() jsonReader, _ := NewConfigReader(readConfigFile("config_testdata/config_iteration_count.json"), ConfigTypeJson) addr, _ := url.Parse("http://proxy_host:80") expectedHammer := types.Hammer{ IterationCount: 1555, LoadType: types.LoadTypeWaved, TestDuration: 21, ReportDestination: report.OutputTypeStdout, Scenario: types.Scenario{ Steps: []types.ScenarioStep{ { ID: 1, Name: "Example Name 1", URL: "https://app.servdown.com/accounts/login/?next=/", Method: http.MethodGet, Timeout: 3, Sleep: "1000", Payload: "payload str", Custom: map[string]interface{}{}, }, { ID: 2, Name: "Example Name 2", URL: "http://test.com", Method: http.MethodPut, Timeout: 2, Sleep: "300-500", Headers: map[string]string{ "ContenType": "application/xml", "X-ddosify-key": "ajkndalnasd", }, }, }, }, Proxy: proxy.Proxy{ Strategy: "single", Addr: addr, }, SamplingRate: types.DefaultSamplingCount, EngineMode: types.EngineModeDdosify, TestDataConf: make(map[string]types.CsvConf), SingleMode: true, } h, err := jsonReader.CreateHammer() if err != nil { t.Errorf("TestCreateHammer error occurred: %v", err) } if !reflect.DeepEqual(expectedHammer, h) { t.Errorf("\nExpected: %v,\n Found: %v", expectedHammer, h) } } func TestCreateHammerWithIterationCountOverridesReqCount(t *testing.T) { t.Parallel() jsonReader, _ := NewConfigReader(readConfigFile("config_testdata/config_iteration_count_over_req_count.json"), ConfigTypeJson) addr, _ := url.Parse("http://proxy_host:80") expectedHammer := types.Hammer{ IterationCount: 333, LoadType: types.LoadTypeWaved, TestDuration: 21, ReportDestination: report.OutputTypeStdout, Scenario: types.Scenario{ Steps: []types.ScenarioStep{ { ID: 1, Name: "Example Name 1", URL: "https://app.servdown.com/accounts/login/?next=/", // Protocol: types.ProtocolHTTPS, Method: http.MethodGet, Timeout: 3, Sleep: "1000", Payload: "payload str", Custom: map[string]interface{}{}, }, { ID: 2, Name: "Example Name 2", URL: "http://test.com", // Protocol: types.ProtocolHTTP, Method: http.MethodPut, Timeout: 2, Sleep: "300-500", Headers: map[string]string{ "ContenType": "application/xml", "X-ddosify-key": "ajkndalnasd", }, }, }, }, Proxy: proxy.Proxy{ Strategy: "single", Addr: addr, }, SamplingRate: types.DefaultSamplingCount, EngineMode: types.EngineModeDdosify, TestDataConf: make(map[string]types.CsvConf), SingleMode: true, } h, err := jsonReader.CreateHammer() if err != nil { t.Errorf("TestCreateHammer error occurred: %v", err) } if !reflect.DeepEqual(expectedHammer, h) { t.Errorf("\nExpected: %v,\n Found: %v", expectedHammer, h) } } func TestCreateHammerManualLoad(t *testing.T) { t.Parallel() jsonReader, _ := NewConfigReader(readConfigFile("config_testdata/config_manual_load.json"), ConfigTypeJson) expectedHammer := types.Hammer{ IterationCount: 35, LoadType: types.DefaultLoadType, TestDuration: 18, TimeRunCountMap: types.TimeRunCount{{Duration: 5, Count: 5}, {Duration: 6, Count: 10}, {Duration: 7, Count: 20}}, ReportDestination: types.DefaultOutputType, Scenario: types.Scenario{ Steps: []types.ScenarioStep{{ ID: 1, URL: "test.com", Method: types.DefaultMethod, Timeout: types.DefaultTimeout, }}, }, Proxy: proxy.Proxy{ Strategy: proxy.ProxyTypeSingle, }, SamplingRate: types.DefaultSamplingCount, EngineMode: types.EngineModeDdosify, TestDataConf: make(map[string]types.CsvConf), SingleMode: true, } h, err := jsonReader.CreateHammer() if err != nil { t.Errorf("TestCreateHammerManualLoad error occurred: %v", err) } if !reflect.DeepEqual(expectedHammer, h) { t.Errorf("Expected: %v, Found: %v", expectedHammer, h) } } func TestCreateHammerManualLoadOverrideOthers(t *testing.T) { t.Parallel() jsonReader, _ := NewConfigReader(readConfigFile("config_testdata/config_manual_load_override.json"), ConfigTypeJson) expectedHammer := types.Hammer{ IterationCount: 35, LoadType: types.DefaultLoadType, TestDuration: 18, TimeRunCountMap: types.TimeRunCount{{Duration: 5, Count: 5}, {Duration: 6, Count: 10}, {Duration: 7, Count: 20}}, ReportDestination: types.DefaultOutputType, Scenario: types.Scenario{ Steps: []types.ScenarioStep{{ ID: 1, URL: "test.com", Method: types.DefaultMethod, Timeout: types.DefaultTimeout, }}, }, Proxy: proxy.Proxy{ Strategy: proxy.ProxyTypeSingle, }, SamplingRate: types.DefaultSamplingCount, EngineMode: types.EngineModeDdosify, TestDataConf: make(map[string]types.CsvConf), SingleMode: true, } h, err := jsonReader.CreateHammer() if err != nil { t.Errorf("TestCreateHammerManualLoad error occurred: %v", err) } if !reflect.DeepEqual(expectedHammer, h) { t.Errorf("Expected: %v, Found: %v", expectedHammer, h) } } func TestCreateHammerPayload(t *testing.T) { t.Parallel() jsonReader, _ := NewConfigReader(readConfigFile("config_testdata/config_payload.json"), ConfigTypeJson) expectedPayloads := []string{"payload from string", "Payloaf from file."} h, err := jsonReader.CreateHammer() if err != nil { t.Errorf("TestCreateHammerPayload error occurred: %v", err) } steps := h.Scenario.Steps if steps[0].Payload != expectedPayloads[0] { t.Errorf("Expected: %v, Found: %v", expectedPayloads[0], steps[0].Payload) } if steps[1].Payload != expectedPayloads[1] { t.Errorf("Expected: %v, Found: %v", expectedPayloads[1], steps[1].Payload) } } func TestCreateHammerMultipartPayload(t *testing.T) { t.Parallel() jsonReader, _ := NewConfigReader(readConfigFile("config_testdata/config_multipart_payload.json"), ConfigTypeJson) h, err := jsonReader.CreateHammer() if err != nil { t.Errorf("TestCreateHammerMultipartPayload error occurred: %v", err) } steps := h.Scenario.Steps // Content-Type Header Check val, ok := steps[0].Headers["Content-Type"] if !ok { t.Error("Content-Type header should be exist") } rgx := "multipart/form-data; boundary=.*" r, _ := regexp.Compile(rgx) if !r.MatchString(val) { t.Errorf("Expected: %v, Found: %v", rgx, val) } // Payload Check - Ensure that payload contains 4 form field. if c := strings.Count(steps[0].Payload, "Content-Disposition: form-data;"); c != 4 { t.Errorf("Expected: %v, Found: %v", 4, c) } } func TestCreateHammerMultipartPayload_RemoteErr(t *testing.T) { t.Parallel() jsonReader, _ := NewConfigReader(readConfigFile("config_testdata/config_multipart_err.json"), ConfigTypeJson) _, err := jsonReader.CreateHammer() if err == nil { t.Error("TestCreateHammerMultipartPayload_RemoteErr should return error") } var multipartErr RemoteMultipartError if !errors.As(err, &multipartErr) { t.Errorf("Expected: %v, Found: %v", multipartErr, err) } } func TestCreateHammerAuth(t *testing.T) { t.Parallel() jsonReader, _ := NewConfigReader(readConfigFile("config_testdata/config_auth.json"), ConfigTypeJson) expectedAuths := []types.Auth{ { Type: types.AuthHttpBasic, Username: "kursat", Password: "12345", }, {}} h, err := jsonReader.CreateHammer() if err != nil { t.Errorf("TestCreateHammerAuth error occurred: %v", err) } steps := h.Scenario.Steps if steps[0].Auth != expectedAuths[0] { t.Errorf("Expected: %v, Found: %v", expectedAuths[0], steps[0].Auth) } if steps[1].Auth != expectedAuths[1] { t.Errorf("Expected: %v, Found: %v", expectedAuths[1], steps[1].Auth) } } func TestCreateHammerGlobalEnvs(t *testing.T) { t.Parallel() jsonReader, _ := NewConfigReader(readConfigFile("config_testdata/config_global_envs.json"), ConfigTypeJson) expectedGlobalEnvs := map[string]interface{}{ "HTTPBIN": "https://httpbin.ddosify.com", "LOCAL": "http://localhost:8084/hello", } h, err := jsonReader.CreateHammer() if err != nil { t.Errorf("TestCreateHammerGlobalEnvs error occurred: %v", err) } globalEnvs := h.Scenario.Envs if !reflect.DeepEqual(globalEnvs, expectedGlobalEnvs) { t.Errorf("TestCreateHammerGlobalEnvs global envs got: %#v expected: %#v", globalEnvs, expectedGlobalEnvs) } } func TestCreateHammerCaptureEnvs(t *testing.T) { t.Parallel() jsonReader, _ := NewConfigReader(readConfigFile("config_testdata/config_capture_environment.json"), ConfigTypeJson) json_path := "num" cookie_name := "x" expectedEnvsToCaptureFirstStep := []types.EnvCaptureConf{ { Name: "NUM", From: types.Body, JsonPath: &json_path, }, { Name: "X_COOKIE", From: types.Cookie, CookieName: &cookie_name, }} regex := "[a-z]+_[0-9]+" expectedEnvsToCaptureSecondStep := []types.EnvCaptureConf{{ Name: "REGEX_MATCH_ENV", From: types.Body, RegExp: &types.RegexCaptureConf{ Exp: ®ex, No: 1, }, }} h, err := jsonReader.CreateHammer() if err != nil { t.Errorf("TestCreateHammerCaptureEnvs error occurred: %v", err) } envsToCapture0 := h.Scenario.Steps[0].EnvsToCapture var numCapturedEnv, xcookieCaptureEnv types.EnvCaptureConf for i := range envsToCapture0 { if envsToCapture0[i].Name == "NUM" { numCapturedEnv = envsToCapture0[i] } else if envsToCapture0[i].Name == "X_COOKIE" { xcookieCaptureEnv = envsToCapture0[i] } } if numCapturedEnv.Name != "NUM" || numCapturedEnv.From != types.Body || *numCapturedEnv.JsonPath != "num" { t.Errorf("TestCreateHammerCaptureEnvs global envs got: %#v expected: %#v", numCapturedEnv, expectedEnvsToCaptureFirstStep[0]) } if xcookieCaptureEnv.Name != "X_COOKIE" || xcookieCaptureEnv.From != types.Cookie || *xcookieCaptureEnv.CookieName != "x" { t.Errorf("TestCreateHammerCaptureEnvs global envs got: %#v expected: %#v", xcookieCaptureEnv, expectedEnvsToCaptureFirstStep[1]) } envsToCaptureSecondStep := h.Scenario.Steps[1].EnvsToCapture var regexMatchEnv types.EnvCaptureConf for i := range envsToCaptureSecondStep { if envsToCaptureSecondStep[i].Name == "REGEX_MATCH_ENV" { regexMatchEnv = envsToCaptureSecondStep[i] } } if regexMatchEnv.Name != "REGEX_MATCH_ENV" || regexMatchEnv.From != types.Body || *regexMatchEnv.RegExp.Exp != "[a-z]+_[0-9]+" || regexMatchEnv.RegExp.No != 1 { t.Errorf("TestCreateHammerCaptureEnvs global envs got: %#v expected: %#v", regexMatchEnv, expectedEnvsToCaptureSecondStep[0]) } } func TestCreateHammerInvalidTarget(t *testing.T) { t.Parallel() jsonReader, _ := NewConfigReader(readConfigFile("config_testdata/config_invalid_target.json"), ConfigTypeJson) _, err := jsonReader.CreateHammer() if err == nil { t.Errorf("TestCreateHammerProtocol error occurred") } } func TestCreateHammerCookiesEnabledValidOnlyOnUserModes(t *testing.T) { t.Parallel() jsonReader, _ := NewConfigReader(readConfigFile("config_testdata/config_invalid_user_mode_for_cookies.json"), ConfigTypeJson) _, err := jsonReader.CreateHammer() if err == nil { t.Errorf("TestCreateHammerCookiesEnabledValidOnlyOnUserModes expected error but got nil, cookies enabled only on user modes") } } func TestCreateHammerTLS(t *testing.T) { t.Parallel() // prepare TLS files cert, certKey := generateCerts() certFile, keyFile, err := createCertPairFiles(cert, certKey) if err != nil { t.Fatalf("Failed to prepare certs %v", err) } defer os.Remove(certFile.Name()) defer os.Remove(keyFile.Name()) config := buildJSONTLSConfig(certFile.Name(), keyFile.Name()) jsonReader, _ := NewConfigReader(config, ConfigTypeJson) h, err := jsonReader.CreateHammer() if err != nil { t.Errorf("TestCreateHammerDefaultValues error occurred: %v", err) } certVal, _, err := types.ParseTLS(certFile.Name(), keyFile.Name()) if err != nil { t.Fatalf("Failed to gen certs %v", err) } // We compare only Certificte because CertPool has pointers inside and it's hard to compare it if !reflect.DeepEqual(certVal, h.Scenario.Steps[0].Cert) { t.Errorf("\nExpected: %#v, \nFound: %#v", certVal, h.Scenario.Steps[0].Cert) } } func TestCreateHammerTLSWithOnlyCertPath(t *testing.T) { t.Parallel() // prepare TLS files cert, certKey := generateCerts() certFile, keyFile, err := createCertPairFiles(cert, certKey) if err != nil { t.Fatalf("Failed to prepare certs %v", err) } defer os.Remove(certFile.Name()) defer os.Remove(keyFile.Name()) config := buildJSONTLSConfig(certFile.Name(), "") jsonReader, _ := NewConfigReader(config, ConfigTypeJson) expectedHammer := types.Hammer{ IterationCount: types.DefaultIterCount, LoadType: types.DefaultLoadType, TestDuration: types.DefaultDuration, ReportDestination: types.DefaultOutputType, Scenario: types.Scenario{ Steps: []types.ScenarioStep{{ ID: 1, URL: "test.com", Method: types.DefaultMethod, Timeout: types.DefaultTimeout, }}, }, Proxy: proxy.Proxy{ Strategy: proxy.ProxyTypeSingle, }, SamplingRate: types.DefaultSamplingCount, EngineMode: types.EngineModeDdosify, TestDataConf: make(map[string]types.CsvConf), SingleMode: true, } h, err := jsonReader.CreateHammer() if err != nil { t.Errorf("TestCreateHammerDefaultValues error occurred: %v", err) } if !reflect.DeepEqual(expectedHammer, h) { t.Errorf("\nExpected: %#v, \nFound: %#v", expectedHammer, h) } } func TestCreateHammerTLSWithOnlyKeyPath(t *testing.T) { t.Parallel() // prepare TLS files cert, certKey := generateCerts() certFile, keyFile, err := createCertPairFiles(cert, certKey) if err != nil { t.Fatalf("Failed to prepare certs %v", err) } defer os.Remove(certFile.Name()) defer os.Remove(keyFile.Name()) config := buildJSONTLSConfig("", keyFile.Name()) jsonReader, _ := NewConfigReader(config, ConfigTypeJson) expectedHammer := types.Hammer{ IterationCount: types.DefaultIterCount, LoadType: types.DefaultLoadType, TestDuration: types.DefaultDuration, ReportDestination: types.DefaultOutputType, Scenario: types.Scenario{ Steps: []types.ScenarioStep{{ ID: 1, URL: "test.com", Method: types.DefaultMethod, Timeout: types.DefaultTimeout, }}, }, Proxy: proxy.Proxy{ Strategy: proxy.ProxyTypeSingle, }, SamplingRate: types.DefaultSamplingCount, EngineMode: types.EngineModeDdosify, TestDataConf: make(map[string]types.CsvConf), SingleMode: true, } h, err := jsonReader.CreateHammer() if err != nil { t.Errorf("TestCreateHammerDefaultValues error occurred: %v", err) } if !reflect.DeepEqual(expectedHammer, h) { t.Errorf("\nExpected: %#v, \nFound: %#v", expectedHammer, h) } } func TestCreateHammerTLSWithWithEmptyPath(t *testing.T) { t.Parallel() config := buildJSONTLSConfig("", "") jsonReader, _ := NewConfigReader(config, ConfigTypeJson) expectedHammer := types.Hammer{ IterationCount: types.DefaultIterCount, LoadType: types.DefaultLoadType, TestDuration: types.DefaultDuration, ReportDestination: types.DefaultOutputType, Scenario: types.Scenario{ Steps: []types.ScenarioStep{{ ID: 1, URL: "test.com", Method: types.DefaultMethod, Timeout: types.DefaultTimeout, }}, }, Proxy: proxy.Proxy{ Strategy: proxy.ProxyTypeSingle, }, SamplingRate: types.DefaultSamplingCount, EngineMode: types.EngineModeDdosify, TestDataConf: make(map[string]types.CsvConf), SingleMode: true, } h, err := jsonReader.CreateHammer() if err != nil { t.Errorf("TestCreateHammerDefaultValues error occurred: %v", err) } if !reflect.DeepEqual(expectedHammer, h) { t.Errorf("\nExpected: %#v, \nFound: %#v", expectedHammer, h) } } func buildJSONTLSConfig(certPath, keyPath string) []byte { format := ` { "steps": [ { "id": 1, "url": "test.com", "cert_path": %q, "cert_key_path": %q } ] }` config := fmt.Sprintf(format, certPath, keyPath) fmt.Println(config) return []byte(config) } func createCertPairFiles(cert string, certKey string) (*os.File, *os.File, error) { certFile, err := os.CreateTemp("", ".pem") if err != nil { return nil, nil, err } _, err = io.WriteString(certFile, cert) if err != nil { return nil, nil, err } keyFile, err := os.CreateTemp("", ".pem") if err != nil { return nil, nil, err } _, err = io.WriteString(keyFile, certKey) if err != nil { return nil, nil, err } return certFile, keyFile, nil } func generateCerts() (string, string) { cert := `-----BEGIN CERTIFICATE----- MIIDazCCAlOgAwIBAgIUS4UhTks8aRCQ1k9IGn437ZyP3MgwDQYJKoZIhvcNAQEL BQAwRTELMAkGA1UEBhMCQVUxEzARBgNVBAgMClNvbWUtU3RhdGUxITAfBgNVBAoM GEludGVybmV0IFdpZGdpdHMgUHR5IEx0ZDAeFw0yMjEwMDUyMjM5MDVaFw0zMjEw MDIyMjM5MDVaMEUxCzAJBgNVBAYTAkFVMRMwEQYDVQQIDApTb21lLVN0YXRlMSEw HwYDVQQKDBhJbnRlcm5ldCBXaWRnaXRzIFB0eSBMdGQwggEiMA0GCSqGSIb3DQEB AQUAA4IBDwAwggEKAoIBAQDMbZctKXBx8v63TXIhM/OB7S6VfPqpzfHufhs6kAHu jfC2ooCUqzqdg0T8bM1bjahYuAbQA1cWKYBsqfd01Po1ltWmbMf7ZvmSB6VN7kC2 Y670zee91dGDQ2yzmorJuIZAtOBVZesYLg8UHSGzSC/smJOrjYidtlbvzOcX0pv3 RCIUrNMed60EpSch/rzAJLzJmwNSQZ4vJHNlNetSkvTi7cxMWfwpcM/rN1hEmP1X J43hJp/TNRZVnEsvs/yggP/FwUjG74mU3KfnWiv91AkkarNTNquEMJ+f4OFqMcnF p0wqg47JTqcAAT0n1B0VB+z0hGXEFMN+IJXsHETZNG+JAgMBAAGjUzBRMB0GA1Ud DgQWBBSIw+qUKQJjXWti5x/Cnn2GueuX5zAfBgNVHSMEGDAWgBSIw+qUKQJjXWti 5x/Cnn2GueuX5zAPBgNVHRMBAf8EBTADAQH/MA0GCSqGSIb3DQEBCwUAA4IBAQAA DXzf8VXi4s2GScNfHf0BzMjpyrtRZ0Wbp2Vfh7OwVR6xcx+pqXNjlydM/vu2LvOK hh7Jbo+JS+o7O24UJ9lLFkCRsZVF+NFqJf+2rdHCaOiZSdZmtjBU0dFuAGS7+lU3 M8P7WCNOm6NAKbs7VZHVcZPzp81SCPQgQIS19xRf4Irbvsijv4YdyL4Qv7aWcclb MdZX9AH9Fx8tJq4VKvUYsCXAD0kuywMLjh+yj5O/2hMvs5rvaQvm2daQNRDNp884 uTLrNF7W7QaKEL06ZpXJoBqdKsiwn577XTDKvzN0XxQrT+xV9VHO7OXblF+Od3/Y SzBR+QiQKy3x+LkOxhkk -----END CERTIFICATE-----` certKey := `-----BEGIN PRIVATE KEY----- MIIEvQIBADANBgkqhkiG9w0BAQEFAASCBKcwggSjAgEAAoIBAQDMbZctKXBx8v63 TXIhM/OB7S6VfPqpzfHufhs6kAHujfC2ooCUqzqdg0T8bM1bjahYuAbQA1cWKYBs qfd01Po1ltWmbMf7ZvmSB6VN7kC2Y670zee91dGDQ2yzmorJuIZAtOBVZesYLg8U HSGzSC/smJOrjYidtlbvzOcX0pv3RCIUrNMed60EpSch/rzAJLzJmwNSQZ4vJHNl NetSkvTi7cxMWfwpcM/rN1hEmP1XJ43hJp/TNRZVnEsvs/yggP/FwUjG74mU3Kfn Wiv91AkkarNTNquEMJ+f4OFqMcnFp0wqg47JTqcAAT0n1B0VB+z0hGXEFMN+IJXs HETZNG+JAgMBAAECggEAM+U6NHfJmNPD/8qER5OFpJ0Ob1qL06F5Yj7XMLWwF9wm mGaGV7dkKOpTD/Wa6Dv82ZDWAeZnLDQa6vr228zZO9Nvp1EEL3kDsCOKvk7WVLbX ikPfKZznE/iA1tNLmkvioPiJ3oQB+2Bt6YA/tuCDcf+FtU43uTm5tiSBIdYQS+Om xN9OEXihk1svxHXQKa/a3nKPVLvdp3P90hDJ0PcRslXSy1V8az+A94JFEnCvnKsK nF2rItCcXkInL0lYHZKgLHQMXGWkNl8e3PA1GZk3yF6LPNtPI1T5Ek9GwkHNw4JZ BL/xEWLKB1qR2Z4I3UbWGVyi418kANv1eISb+49egQKBgQDraSRWB8nM5O3Zl9kT 8S5K924o1oXrO17eqQlVtQVmtUdoVvIBc6uHQZOmV1eHYpr6c95h8apNLexI22AY SWkq9smpCnxLUsdkplwzie0F4bAzD6MCR8WIJxapUSPlyCA+8st1hquYBchKGQhd 6mMY1gzMDacYV/WhtG4E5d0nMQKBgQDeTr793n00VtpKuquFJe6Stu7Ujf64dL0s 3opLovyI0TmtMz5oCqIezwrjqc0Vy0UksWXaz0AboinDP+5n60cTEIt/6H0kryDc dxfSHEA9BBDoQtxOFi3QGcxXbwu0i9QSoexrKY7FhA2xPji6bCcPycthhIrCpUiZ s5gVkjHn2QKBgQCGklxLMbiSgGvXb46Qb9be1AMNJVT427+n2UmUzR6BUC+53boK Sm1LrJkTBerrYdrmQUZnBxcrd40TORT9zTlpbhppn6zeAjwptVAPxlDQg+uNxOqS ayToaC/0KoYy3OxSD8lvLcT56pRMh3LY/RwZHoPCQiu7Js0r21DpS93YgQKBgAuc c09RMprsOmSS0WiX7ZkOIvVJIVfDCSpxySlgLu56dxe7yHOosoUHbVsswEB2KHtd JKPEFWYcFzBSg4I8AK9XOuIIY5jp6L57Hexke1p0fumSrG0LrYLkBg8/Bo58iywZ 9v414nYgipKKXG4oPfYOJShHwvOdrGgSwEvIIgEpAoGAZz0yC9+x+JaoTnyUIRyI +Aj5a4KhYjFtsZhcn/yCZHDqzJNDz6gAu579ey+J2CVOhjtgB5lowsDrHu32Hqnn SEfyTru/ynQ8obwaRzdDYml+On86YWOw+brpMXkN+KB6bs2okE2N68v0qGPakxjt OLDW6kKz5pI4T8lQJhdqjCU= -----END PRIVATE KEY-----` return cert, certKey } ================================================ FILE: ddosify_engine/config_examples/assertion/expected_body.json ================================================ [ "AED", "ARS", "AUD", "BGN", "BHD", "BRL", "CAD", "CHF", "CNY", "DKK", "DZD", "EUR", "FKP", "INR", "JEP", "JPY", "KES", "KWD", "KZT", "MXN", "NZD", "RUB", "SEK", "SGD", "TRY", "USD" ] ================================================ FILE: ddosify_engine/config_examples/config.json ================================================ // This file contains full features of Ddosify as a reference. Don't use it directly. { "request_count": 30, // This field will be deprecated, please use iteration_count instead. "iteration_count": 30, "debug" : false, // use this field for debugging, see verbose result "load_type": "linear", "engine_mode": "distinct-user", // could be one of "distinct-user","repeated-user", or default mode "ddosify" "duration": 5, "manual_load": [ {"duration": 5, "count": 5}, {"duration": 6, "count": 10}, {"duration": 7, "count": 20} ], "env" : { "HTTPBIN" : "https://httpbin.ddosify.com", "LOCAL" : "http://localhost:8084", "NAMES" : ["kenan","fatih","kursat","semih","sertac"] , "NUMBERS" : [52,99,60,33], "BOOLS" : [true,true,true,false], "randomIntPerIteration": "{{_randomInt}}" }, "data":{ "info": { "path" : "config/config_testdata/test.csv", "delimiter": ";", "vars": { "0":{"tag":"name"}, "1":{"tag":"city"}, "2":{"tag":"team"}, "3":{"tag":"payload", "type":"json"}, "4":{"tag":"age", "type":"int"} }, "allow_quota" : true, "order": "random", "skip_first_line" : true, "skip_empty_line" : true } }, "success_criterias": [ { "rule" : "p90(iteration_duration) < 220", "abort" : false }, { "rule" : "fail_count_perc < 0.1", "abort" : true, "delay" : 1 }, { "rule" : "fail_count < 100", "abort" : true, "delay" : 0 } ], "proxy": "http://proxy_host.com:proxy_port", "output": "stdout", "steps": [ { "id": 1, "url": "https://getanteon.com/endpoint_1", "method": "POST", "headers": { "Content-Type": "application/xml", "header1": "header2" }, "payload": "Body content 1", "timeout": 3, "sleep": "300-500", "auth": { "username": "test_user", "password": "12345" }, "others": { "disable-compression": false, "h2": true, "disable-redirect": true }, "capture_env": { "NUM" :{ "from":"body","json_path":"num"} }, "assertion":[ "equals(status_code,200)", "in(variables.num,[10,20])" ] }, { "id": 2, "url": "{{LOCAL}}", "method": "GET", "payload_file": "config_examples/payload.txt", "timeout": 2, "sleep": "1000", "headers":{ "num": "{{NUM}}", "randNum": "{{rand(NUMBERS)}}", "randInt" : "{{randomIntPerIteration}}" }, "assertion":[ "contains(body,\"xyz\")", "range(headers.content-length,1000,10000)" ] }, { "id": 3, "url": "https://getanteon.com/endpoint_3", "method": "POST", "payload_multipart": [ { "name": "[field-name]", "value": "[field-value]" }, { "name": "[field-name]", "value": "./test.png", "type": "file" }, { "name": "[field-name]", "value": "http://test.com/test.png", "type": "file", "src": "remote" } ], "timeout": 2 } ] } ================================================ FILE: ddosify_engine/config_examples/payload.txt ================================================ body file 1111111111 body file 22222222222 ================================================ FILE: ddosify_engine/core/assertion/base.go ================================================ package assertion import ( "go.ddosify.com/ddosify/core/types" ) type Aborter interface { AbortChan() <-chan struct{} } type ResultListener interface { Start(input <-chan *types.ScenarioResult) DoneChan() <-chan struct{} // indicates processing of results are done } type Asserter interface { ResultChan() <-chan TestAssertionResult } ================================================ FILE: ddosify_engine/core/assertion/service.go ================================================ package assertion import ( "sort" "sync" "time" "go.ddosify.com/ddosify/core/scenario/scripting/assertion" "go.ddosify.com/ddosify/core/scenario/scripting/assertion/evaluator" "go.ddosify.com/ddosify/core/types" "golang.org/x/exp/slices" ) var tickerInterval = 100 // interval in millisecond type DefaultAssertionService struct { assertions map[string]types.TestAssertionOpt // Rule -> Opts abortChan chan struct{} doneChan chan struct{} resChan chan TestAssertionResult assertEnv *evaluator.AssertEnv abortTick map[string]int // rule -> tickIndex iterCount int mu sync.Mutex } type TestAssertionResult struct { Fail bool `json:"fail"` Aborted bool `json:"aborted"` FailedRules []FailedRule `json:"failed_rules"` } type FailedRule struct { Rule string `json:"rule"` ReceivedMap map[string]interface{} `json:"received"` } func NewDefaultAssertionService() (service *DefaultAssertionService) { return &DefaultAssertionService{} } func (as *DefaultAssertionService) Init(assertions map[string]types.TestAssertionOpt) chan struct{} { as.assertions = assertions as.abortChan = make(chan struct{}) as.doneChan = make(chan struct{}) as.resChan = make(chan TestAssertionResult, 1) totalTime := make([]int64, 0) as.assertEnv = &evaluator.AssertEnv{TotalTime: totalTime} as.abortTick = make(map[string]int) as.mu = sync.Mutex{} return as.abortChan } func (as *DefaultAssertionService) GetTotalTimes() []int64 { return as.assertEnv.TotalTime } func (as *DefaultAssertionService) GetFailCount() int { return as.assertEnv.FailCount } func (as *DefaultAssertionService) Start(input <-chan *types.ScenarioResult) { // get iteration results, add store them cumulatively firstResult := true for r := range input { as.mu.Lock() as.aggregate(r) as.mu.Unlock() // after first result start checking assertions if firstResult { go as.applyAssertions() firstResult = false } } as.resChan <- as.giveFinalResult() as.doneChan <- struct{}{} } func (as *DefaultAssertionService) aggregate(r *types.ScenarioResult) { var iterationTime int64 var iterFailed bool as.iterCount++ for _, sr := range r.StepResults { iterationTime += sr.Duration.Milliseconds() if sr.Err.Type != "" || len(sr.FailedAssertions) > 0 { iterFailed = true } } if iterFailed { as.assertEnv.FailCount++ } // keep totalTime array sorted as.insertSorted(iterationTime) as.assertEnv.FailCountPerc = float64(as.assertEnv.FailCount) / float64(as.iterCount) } func (as *DefaultAssertionService) applyAssertions() { ticker := time.NewTicker(time.Duration(tickerInterval) * time.Millisecond) tickIndex := 1 // apply assertions on the fly for only abort:true ones assertionsWithAbort := make(map[string]types.TestAssertionOpt) for rule, opts := range as.assertions { if opts.Abort { assertionsWithAbort[rule] = opts } } for range ticker.C { as.mu.Lock() var totalTime []int64 totalTime = append(totalTime, as.assertEnv.TotalTime...) assertEnv := evaluator.AssertEnv{ TotalTime: totalTime, FailCount: as.assertEnv.FailCount, } as.mu.Unlock() // apply assertions for rule, opts := range assertionsWithAbort { res, _ := assertion.Assert(rule, &assertEnv) if res == false && opts.Abort { // if delay is zero, immediately abort if opts.Delay == 0 || as.abortTick[rule] == tickIndex { as.abortChan <- struct{}{} return } if _, ok := as.abortTick[rule]; !ok { // schedule check at delayTick := (time.Duration(opts.Delay) * time.Second) / (time.Duration(tickerInterval) * time.Millisecond) as.abortTick[rule] = tickIndex + int(delayTick) - 1 } } } tickIndex++ } } func (as *DefaultAssertionService) giveFinalResult() TestAssertionResult { // return final result result := TestAssertionResult{ Fail: false, } failedRules := []FailedRule{} for rule, _ := range as.assertions { res, err := assertion.Assert(rule, as.assertEnv) if res == false { failedRules = append(failedRules, FailedRule{ Rule: rule, ReceivedMap: err.(assertion.AssertionError).Received(), }) } } if len(failedRules) > 0 { result.Fail = true result.FailedRules = failedRules } return result } func (as *DefaultAssertionService) ResultChan() <-chan TestAssertionResult { return as.resChan } func (as *DefaultAssertionService) AbortChan() <-chan struct{} { return as.abortChan } func (as *DefaultAssertionService) DoneChan() <-chan struct{} { return as.doneChan } func (as *DefaultAssertionService) insertSorted(v int64) { index := sort.Search(len(as.assertEnv.TotalTime), func(i int) bool { return as.assertEnv.TotalTime[i] >= v }) as.assertEnv.TotalTime = slices.Insert(as.assertEnv.TotalTime, index, v) } ================================================ FILE: ddosify_engine/core/assertion/service_test.go ================================================ package assertion import ( "reflect" "sort" "sync" "testing" "time" "go.ddosify.com/ddosify/core/types" ) func TestApplyAssertionsAbortsCorrectly(t *testing.T) { service := NewDefaultAssertionService() assertions := make(map[string]types.TestAssertionOpt) rule := "false" delay := 3 assertions[rule] = types.TestAssertionOpt{ Abort: true, Delay: delay, } abortChan := service.Init(assertions) inputChan := make(chan *types.ScenarioResult) go service.Start(inputChan) wg := sync.WaitGroup{} wg.Add(1) go func() { <-abortChan wg.Done() }() inputChan <- &types.ScenarioResult{} start := time.Now() wg.Wait() timePassed := time.Since(start).Seconds() if int(timePassed) != delay { t.Errorf("Delay, got %f, expected %d", timePassed, delay) } } func TestServiceKeepsIterationTimes(t *testing.T) { service := NewDefaultAssertionService() assertions := make(map[string]types.TestAssertionOpt) rule := "false" delay := 3 assertions[rule] = types.TestAssertionOpt{ Abort: false, Delay: delay, } _ = service.Init(assertions) inputChan := make(chan *types.ScenarioResult) go service.Start(inputChan) wg := sync.WaitGroup{} wg.Add(1) go func() { <-service.ResultChan() wg.Done() }() expectedIterationTimes := SortableInt64Slice{} for i := 0; i < 10; i++ { iterTime := time.Duration(((i * 5) % 4) * int(time.Millisecond)) expectedIterationTimes = append(expectedIterationTimes, iterTime.Milliseconds()) inputChan <- &types.ScenarioResult{ StepResults: []*types.ScenarioStepResult{ { StepID: 1, Duration: iterTime, }, }, } } sort.Sort(expectedIterationTimes) close(inputChan) wg.Wait() iterationTimes := service.GetTotalTimes() if !reflect.DeepEqual(iterationTimes, []int64(expectedIterationTimes)) { t.Errorf("TestServiceKeepsIterationTimes, cumulative data store failed") } } func TestServiceKeepsFailCount(t *testing.T) { service := NewDefaultAssertionService() assertions := make(map[string]types.TestAssertionOpt) _ = service.Init(assertions) inputChan := make(chan *types.ScenarioResult) go service.Start(inputChan) wg := sync.WaitGroup{} wg.Add(1) go func() { <-service.ResultChan() wg.Done() }() N := 10 // 2*N times failed iteration result for i := 0; i < N; i++ { inputChan <- &types.ScenarioResult{ StepResults: []*types.ScenarioStepResult{ { StepID: 1, FailedAssertions: []types.FailedAssertion{ { Rule: "failed assertion expression", Received: map[string]interface{}{}, Reason: "", }, }, }, }, } inputChan <- &types.ScenarioResult{ StepResults: []*types.ScenarioStepResult{ { StepID: 1, Err: types.RequestError{ Type: "server error type", Reason: "", }, }, }, } } close(inputChan) wg.Wait() failCount := service.GetFailCount() if failCount != 2*N { t.Errorf("TestServiceKeepsFailCount, expected : %d, got : %d", 2*N, failCount) } } type SortableInt64Slice []int64 func (a SortableInt64Slice) Len() int { return len(a) } func (a SortableInt64Slice) Swap(i, j int) { a[i], a[j] = a[j], a[i] } func (a SortableInt64Slice) Less(i, j int) bool { return a[i] < a[j] } ================================================ FILE: ddosify_engine/core/engine.go ================================================ /* * * Ddosify - Load testing tool for any web system. * Copyright (C) 2021 Ddosify (https://ddosify.com) * * This program is free software: you can redistribute it and/or modify * it under the terms of the GNU Affero General Public License as published * by the Free Software Foundation, either version 3 of the License, or * (at your option) any later version. * * This program is distributed in the hope that it will be useful, * but WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * GNU Affero General Public License for more details. * * You should have received a copy of the GNU Affero General Public License * along with this program. If not, see . * */ package core import ( "context" "fmt" "math" "net/http" "reflect" "sync" "time" "go.ddosify.com/ddosify/core/assertion" "go.ddosify.com/ddosify/core/proxy" "go.ddosify.com/ddosify/core/report" "go.ddosify.com/ddosify/core/scenario" "go.ddosify.com/ddosify/core/scenario/data" "go.ddosify.com/ddosify/core/types" ) const ( // interval in millisecond tickerInterval = 100 // test result status resultDone = "done" resultStopped = "stopped" resultAborted = "aborted" ) type engine struct { hammer types.Hammer proxyService proxy.ProxyService reportService report.ReportService scenarioService *scenario.ScenarioService // for assertion aborter assertion.Aborter asserter assertion.Asserter resListener assertion.ResultListener tickCounter int reqCountArr []int wg sync.WaitGroup resultReportChan chan *types.ScenarioResult resultAssertChan chan *types.ScenarioResult abortChan <-chan struct{} testSuccess bool ctx context.Context } type EngineServices struct { Aborter assertion.Aborter Asserter assertion.Asserter ResListener assertion.ResultListener ProxyServ proxy.ProxyService ReportServ report.ReportService } var InitEngineServices = func(h types.Hammer) (*EngineServices, error) { // Initialize things here and pass interfaces to NewEngine which it depends ? // this piece can change between implementations as := assertion.NewDefaultAssertionService() as.Init(h.Assertions) // TODO: remove reflection ? ps, err := proxy.NewProxyService(h.Proxy.Strategy) if err != nil { return nil, err } err = ps.Init(h.Proxy) if err != nil { return nil, err } // TODO: remove reflection ? rs, err := report.NewReportService(h.ReportDestination) if err != nil { return nil, err } if err = rs.Init(h.Debug, h.SamplingRate); err != nil { return nil, err } return &EngineServices{ // defaultAssertionService as implements all Aborter: as, Asserter: as, ResListener: as, ProxyServ: ps, ReportServ: rs, }, nil } // NewEngine is the constructor of the engine. // Hammer is used for initializing the engine itself and its' external services. // Engine can be stopped by canceling the given ctx. func NewEngine(ctx context.Context, h types.Hammer, services *EngineServices) (e *engine, err error) { ss := scenario.NewScenarioService() e = &engine{ hammer: h, ctx: ctx, proxyService: services.ProxyServ, scenarioService: ss, reportService: services.ReportServ, // for assertion aborter: services.Aborter, resListener: services.ResListener, asserter: services.Asserter, } return } func (e *engine) IsTestFailed() bool { return !e.testSuccess } func (e *engine) Init() (err error) { // read test data readData, err := readTestData(e.hammer.TestDataConf) if err != nil { return err } e.hammer.Scenario.Data = readData e.initReqCountArr() var initialCookies []*http.Cookie if e.hammer.CookiesEnabled && len(e.hammer.Cookies) > 0 { initialCookies, err = createInitialCookies(e.hammer.Cookies) if err != nil { return err } } if err = e.scenarioService.Init(e.ctx, e.hammer.Scenario, e.proxyService.GetAll(), scenario.ScenarioOpts{ Debug: e.hammer.Debug, IterationCount: e.hammer.IterationCount, MaxConcurrentIterCount: e.getMaxConcurrentIterCount(), EngineMode: e.hammer.EngineMode, InitialCookies: initialCookies, }); err != nil { return } e.abortChan = e.aborter.AbortChan() return } func (e *engine) Start() string { ticker := time.NewTicker(time.Duration(tickerInterval) * time.Millisecond) e.resultReportChan = make(chan *types.ScenarioResult, e.hammer.IterationCount) e.resultAssertChan = make(chan *types.ScenarioResult, e.hammer.IterationCount) var testResultChan <-chan assertion.TestAssertionResult if e.runAssertionsInEngine() { // run test wide assertions in parallel testResultChan = e.asserter.ResultChan() } if len(e.hammer.Assertions) > 0 { // test-wide assertions given go e.resListener.Start(e.resultAssertChan) } go e.reportService.Start(e.resultReportChan, testResultChan) defer func() { ticker.Stop() e.stop() }() e.tickCounter = 0 e.wg = sync.WaitGroup{} var mutex = &sync.Mutex{} for range ticker.C { if e.tickCounter >= len(e.reqCountArr) { return resultDone } select { case <-e.ctx.Done(): return resultStopped case <-e.abortChan: e.testSuccess = false return resultAborted default: mutex.Lock() e.wg.Add(e.reqCountArr[e.tickCounter]) go e.runWorkers(e.tickCounter) e.tickCounter++ mutex.Unlock() } } return resultDone } func (e *engine) runWorkers(c int) { for i := 1; i <= e.reqCountArr[c]; i++ { scenarioStartTime := time.Now() go func(t time.Time) { e.runWorker(t) e.wg.Done() }(scenarioStartTime) } } func (e *engine) runWorker(scenarioStartTime time.Time) { var res *types.ScenarioResult var err *types.RequestError p := e.proxyService.GetProxy() retryCount := 3 for i := 1; i <= retryCount; i++ { res, err = e.scenarioService.Do(p, scenarioStartTime) if err != nil && err.Type == types.ErrorProxy { p = e.proxyService.ReportProxy(p, err.Reason) continue } if err != nil && err.Type == types.ErrorIntented { // Don't report intentionally created errors. Like canceled requests. return } break } res.Others = make(map[string]interface{}) res.Others["hammerOthers"] = e.hammer.Others res.Others["proxyCountry"] = e.proxyService.GetProxyCountry(p) e.resultReportChan <- res if len(e.hammer.Assertions) > 0 { e.resultAssertChan <- res } } func (e *engine) runAssertionsInEngine() bool { return e.hammer.SingleMode && len(e.hammer.Assertions) > 0 } func (e *engine) stop() { e.wg.Wait() close(e.resultReportChan) close(e.resultAssertChan) e.proxyService.Done() e.scenarioService.Done() if len(e.hammer.Assertions) > 0 { // if results are listened, wait <-e.resListener.DoneChan() } e.testSuccess = <-e.reportService.DoneChan() } func (e *engine) getMaxConcurrentIterCount() int { max := 0 for _, v := range e.reqCountArr { if v > max { max = v } } return max } func (e *engine) initReqCountArr() { if e.hammer.Debug { e.reqCountArr = []int{1} return } length := int(e.hammer.TestDuration * int(time.Second/(tickerInterval*time.Millisecond))) e.reqCountArr = make([]int, length) if e.hammer.TimeRunCountMap != nil { e.createManualReqCountArr() } else { switch e.hammer.LoadType { case types.LoadTypeLinear: e.createLinearReqCountArr() case types.LoadTypeIncremental: e.createIncrementalReqCountArr() case types.LoadTypeWaved: e.createWavedReqCountArr() } } } func (e *engine) createManualReqCountArr() { tickPerSecond := int(time.Second / (tickerInterval * time.Millisecond)) stepStartIndex := 0 for _, t := range e.hammer.TimeRunCountMap { steps := make([]int, t.Duration) createLinearDistArr(t.Count, steps) for i := range steps { tickArrStartIndex := (i * tickPerSecond) + stepStartIndex tickArrEndIndex := tickArrStartIndex + tickPerSecond segment := e.reqCountArr[tickArrStartIndex:tickArrEndIndex] createLinearDistArr(steps[i], segment) } stepStartIndex += len(steps) * tickPerSecond } } func (e *engine) createLinearReqCountArr() { steps := make([]int, e.hammer.TestDuration) createLinearDistArr(e.hammer.IterationCount, steps) tickPerSecond := int(time.Second / (tickerInterval * time.Millisecond)) for i := range steps { tickArrStartIndex := i * tickPerSecond tickArrEndIndex := tickArrStartIndex + tickPerSecond segment := e.reqCountArr[tickArrStartIndex:tickArrEndIndex] createLinearDistArr(steps[i], segment) } } func (e *engine) createIncrementalReqCountArr() { steps := createIncrementalDistArr(e.hammer.IterationCount, e.hammer.TestDuration) tickPerSecond := int(time.Second / (tickerInterval * time.Millisecond)) for i := range steps { tickArrStartIndex := i * tickPerSecond tickArrEndIndex := tickArrStartIndex + tickPerSecond segment := e.reqCountArr[tickArrStartIndex:tickArrEndIndex] createLinearDistArr(steps[i], segment) } } func (e *engine) createWavedReqCountArr() { tickPerSecond := int(time.Second / (tickerInterval * time.Millisecond)) quarterWaveCount := int((math.Log2(float64(e.hammer.TestDuration)))) if quarterWaveCount == 0 { quarterWaveCount = 1 } qWaveDuration := int(e.hammer.TestDuration / quarterWaveCount) reqCountPerQWave := int(e.hammer.IterationCount / quarterWaveCount) tickArrStartIndex := 0 for i := 0; i < quarterWaveCount; i++ { if i == quarterWaveCount-1 { // Add remaining req count to the last wave reqCountPerQWave += e.hammer.IterationCount - (reqCountPerQWave * quarterWaveCount) } steps := createIncrementalDistArr(reqCountPerQWave, qWaveDuration) if i%2 == 1 { reverse(steps) } for j := range steps { tickArrEndIndex := tickArrStartIndex + tickPerSecond segment := e.reqCountArr[tickArrStartIndex:tickArrEndIndex] createLinearDistArr(steps[j], segment) tickArrStartIndex += tickPerSecond } } } func createLinearDistArr(count int, arr []int) { arrLen := len(arr) minReqCount := int(count / arrLen) remaining := count - minReqCount*arrLen for i := range arr { plusOne := 0 if i < remaining { plusOne = 1 } reqCount := minReqCount + plusOne arr[i] = reqCount } } func createIncrementalDistArr(count int, len int) []int { steps := make([]int, len) sum := (len * (len + 1)) / 2 incrementStep := int(math.Ceil(float64(sum) / float64(count))) val := 0 for i := range steps { if i > 0 { val = steps[i-1] } if i%incrementStep == 0 { steps[i] = val + 1 } else { steps[i] = val } } sum = arraySum(steps) factor := count / sum remaining := count - (sum * factor) plus := remaining / len lastRemaining := remaining - (plus * len) for i := range steps { steps[i] = steps[i]*factor + plus if len-i-1 < lastRemaining { steps[i]++ } } return steps } func arraySum(steps []int) int { sum := 0 for i := range steps { sum += steps[i] } return sum } func reverse(s interface{}) { n := reflect.ValueOf(s).Len() swap := reflect.Swapper(s) for i, j := 0, n-1; i < j; i, j = i+1, j-1 { swap(i, j) } } var readTestData = func(testDataConf map[string]types.CsvConf) (map[string]types.CsvData, error) { // Read Data var readData map[string]types.CsvData if len(testDataConf) > 0 { readData = make(map[string]types.CsvData, len(testDataConf)) } for k, conf := range testDataConf { var rows []map[string]interface{} var err error rows, err = data.ReadCsv(conf) if err != nil { return nil, err } var csvData types.CsvData csvData.Rows = rows if conf.Order == "random" { csvData.Random = true } readData[k] = csvData } return readData, nil } func parseRawCookie(cookie string) []*http.Cookie { header := http.Header{} header.Add("Set-Cookie", cookie) req := http.Response{Header: header} return req.Cookies() } var createInitialCookies = func(cookies []types.CustomCookie) ([]*http.Cookie, error) { initialCookies := make([]*http.Cookie, 0, len(cookies)) for _, c := range cookies { var ck *http.Cookie if c.Raw != "" { cookies := parseRawCookie(c.Raw) if len(cookies) == 0 { return nil, fmt.Errorf("cookie could not be parsed, got : %s", c.Raw) } ck = cookies[0] } else { var expires time.Time if c.Expires != "" { var err error expires, err = time.Parse(time.RFC1123, c.Expires) if err != nil { return nil, fmt.Errorf("error parsing cookie expiry: %s", err) } } ck = &http.Cookie{ Name: c.Name, Value: c.Value, Path: c.Path, Domain: c.Domain, Expires: expires, RawExpires: c.Expires, MaxAge: c.MaxAge, Secure: c.Secure, HttpOnly: c.HttpOnly, Raw: c.Raw, // below fields not used SameSite: 0, Unparsed: []string{}, } } initialCookies = append(initialCookies, ck) } return initialCookies, nil } ================================================ FILE: ddosify_engine/core/engine_test.go ================================================ /* * * Ddosify - Load testing tool for any web system. * Copyright (C) 2021 Ddosify (https://ddosify.com) * * This program is free software: you can redistribute it and/or modify * it under the terms of the GNU Affero General Public License as published * by the Free Software Foundation, either version 3 of the License, or * (at your option) any later version. * * This program is distributed in the hope that it will be useful, * but WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * GNU Affero General Public License for more details. * * You should have received a copy of the GNU Affero General Public License * along with this program. If not, see . * */ package core import ( "bytes" "context" "crypto/tls" "encoding/base64" "encoding/json" "errors" "fmt" "io" "io/ioutil" "net/http" "net/http/httptest" "os" "reflect" "regexp" "strconv" "strings" "sync" "testing" "time" "github.com/ddosify/go-faker/faker" "go.ddosify.com/ddosify/config" "go.ddosify.com/ddosify/core/proxy" "go.ddosify.com/ddosify/core/report" "go.ddosify.com/ddosify/core/types" ) //TODO: Engine stop channel close order test func newDummyHammer() types.Hammer { return types.Hammer{ Proxy: proxy.Proxy{Strategy: proxy.ProxyTypeSingle}, ReportDestination: report.OutputTypeStdout, LoadType: types.LoadTypeLinear, TestDuration: 1, IterationCount: 1, Scenario: types.Scenario{ Steps: []types.ScenarioStep{ { ID: 1, Method: "GET", URL: "http://127.0.0.1", }, }, }, SingleMode: true, } } func TestCreateEngine(t *testing.T) { t.Parallel() hInvalidProxy := newDummyHammer() hInvalidProxy.Proxy = proxy.Proxy{Strategy: "invalidProxy"} hInvalidReport := newDummyHammer() hInvalidReport.ReportDestination = "invalidReport" tests := []struct { name string hammer types.Hammer shouldErr bool }{ {"Normal", newDummyHammer(), false}, {"InvalidProxy", hInvalidProxy, true}, {"InvalidReport", hInvalidReport, true}, } for _, tc := range tests { test := tc t.Run(test.name, func(t *testing.T) { t.Parallel() es, err := InitEngineServices(test.hammer) // e, err := NewEngine(context.TODO(), test.hammer, es) if test.shouldErr { if err == nil { t.Errorf("Should be errored") } } else { if err != nil { t.Errorf("Error occurred %v", err) } if es.ProxyServ == nil { t.Errorf("Proxy Service should be created") } // TODOr: not an interface ? // if es.scenarioService == nil { // t.Errorf("Scenario Service should be created") // } if es.ReportServ == nil { t.Errorf("Report Service should be created") } } }) } } func TestReqCountArrDebugMode(t *testing.T) { t.Parallel() hammer := newDummyHammer() hammer.Debug = true tests := []struct { name string hammer types.Hammer }{ {"DebugMode", hammer}, } for _, tc := range tests { test := tc t.Run(test.name, func(t *testing.T) { t.Parallel() es, err := InitEngineServices(test.hammer) e, err := NewEngine(context.TODO(), test.hammer, es) e.Init() if err != nil { t.Errorf("Should have been nil, got %v", err) } // one iteration one tick if !reflect.DeepEqual(e.reqCountArr, []int{1}) { t.Errorf("Debug mode reqCountArr should have only one iteration in one tick, got %v", e.reqCountArr) } }) } } // TODO: Add other load types as you implement func TestRequestCount(t *testing.T) { t.Parallel() tests := []struct { name string loadType string duration int reqCount int timeRunCount types.TimeRunCount expectedReqArr []int delta int }{ {"Linear1", types.LoadTypeLinear, 1, 100, nil, []int{10, 10, 10, 10, 10, 10, 10, 10, 10, 10}, 1}, {"Linear2", types.LoadTypeLinear, 1, 5, nil, []int{1, 1, 1, 1, 1, 0, 0, 0, 0, 0}, 0}, {"Linear3", types.LoadTypeLinear, 2, 4, nil, []int{1, 1, 0, 0, 0, 0, 0, 0, 0, 0, 1, 1, 0, 0, 0, 0, 0, 0, 0, 0}, 0}, {"Linear4", types.LoadTypeLinear, 2, 23, nil, []int{2, 2, 1, 1, 1, 1, 1, 1, 1, 1, 2, 1, 1, 1, 1, 1, 1, 1, 1, 1}, 0}, {"Incremental1", types.LoadTypeIncremental, 1, 5, nil, []int{1, 1, 1, 1, 1, 0, 0, 0, 0, 0}, 2}, {"Incremental2", types.LoadTypeIncremental, 3, 1022, nil, []int{17, 17, 17, 17, 17, 17, 17, 17, 17, 17, 35, 34, 34, 34, 34, 34, 34, 34, 34, 34, 52, 51, 51, 51, 51, 51, 51, 51, 51, 51}, 2}, {"Incremental3", types.LoadTypeIncremental, 5, 10, nil, []int{1, 0, 0, 0, 0, 0, 0, 0, 0, 0, 1, 0, 0, 0, 0, 0, 0, 0, 0, 0, 1, 1, 0, 0, 0, 0, 0, 0, 0, 0, 1, 1, 0, 0, 0, 0, 0, 0, 0, 0, 1, 1, 1, 1, 0, 0, 0, 0, 0, 0}, 0}, {"Incremental4", types.LoadTypeIncremental, 4, 10, nil, []int{1, 0, 0, 0, 0, 0, 0, 0, 0, 0, 1, 1, 0, 0, 0, 0, 0, 0, 0, 0, 1, 1, 1, 0, 0, 0, 0, 0, 0, 0, 1, 1, 1, 1, 0, 0, 0, 0, 0, 0}, 0}, {"Waved1", types.LoadTypeWaved, 1, 5, nil, []int{1, 1, 1, 1, 1, 0, 0, 0, 0, 0}, 0}, {"Waved2", types.LoadTypeWaved, 4, 32, nil, []int{1, 1, 1, 1, 1, 0, 0, 0, 0, 0, 2, 1, 1, 1, 1, 1, 1, 1, 1, 1, 2, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 0, 0, 0, 0, 0}, 0}, {"Waved3", types.LoadTypeWaved, 5, 10, nil, []int{1, 1, 0, 0, 0, 0, 0, 0, 0, 0, 1, 1, 1, 0, 0, 0, 0, 0, 0, 0, 1, 1, 1, 0, 0, 0, 0, 0, 0, 0, 1, 1, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0}, 0}, {"Waved4", types.LoadTypeWaved, 9, 1000, nil, []int{6, 6, 6, 6, 6, 6, 5, 5, 5, 5, 12, 11, 11, 11, 11, 11, 11, 11, 11, 11, 17, 17, 17, 17, 17, 17, 16, 16, 16, 16, 17, 17, 17, 17, 17, 17, 16, 16, 16, 16, 12, 11, 11, 11, 11, 11, 11, 11, 11, 11, 6, 6, 6, 6, 6, 6, 5, 5, 5, 5, 6, 6, 6, 6, 6, 6, 5, 5, 5, 5, 12, 11, 11, 11, 11, 11, 11, 11, 11, 11, 17, 17, 17, 17, 17, 17, 17, 16, 16, 16}, 1}, {"TimeRunCount1", "", 1, 100, types.TimeRunCount{{Duration: 1, Count: 100}}, []int{10, 10, 10, 10, 10, 10, 10, 10, 10, 10}, 1}, {"TimeRunCount2", "", 1, 5, types.TimeRunCount{{Duration: 1, Count: 5}}, []int{1, 1, 1, 1, 1, 0, 0, 0, 0, 0}, 0}, {"TimeRunCount3", "", 6, 55, types.TimeRunCount{{Duration: 1, Count: 20}, {Duration: 2, Count: 30}, {Duration: 3, Count: 5}}, []int{2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 1, 1, 1, 1, 1, 2, 2, 2, 2, 2, 1, 1, 1, 1, 1, 1, 1, 0, 0, 0, 0, 0, 0, 0, 0, 1, 1, 0, 0, 0, 0, 0, 0, 0, 0, 1, 0, 0, 0, 0, 0, 0, 0, 0, 0}, 0}, {"TimeRunCount4", "", 5, 40, types.TimeRunCount{{Duration: 1, Count: 20}, {Duration: 2, Count: 0}, {Duration: 2, Count: 20}}, []int{2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1}, 0}, } for _, tc := range tests { test := tc t.Run(test.name, func(t *testing.T) { t.Parallel() var timeReqMap map[int]int var now time.Time var m sync.Mutex // Test server handler := func(w http.ResponseWriter, r *http.Request) { m.Lock() i := time.Since(now).Milliseconds()/tickerInterval - 1 timeReqMap[int(i)]++ m.Unlock() } server := httptest.NewServer(http.HandlerFunc(handler)) defer server.Close() // Prepare h := newDummyHammer() h.LoadType = test.loadType h.TestDuration = test.duration h.TimeRunCountMap = test.timeRunCount h.IterationCount = test.reqCount h.Scenario.Steps[0].URL = server.URL now = time.Now() timeReqMap = make(map[int]int, 0) es, err := InitEngineServices(h) e, err := NewEngine(context.TODO(), h, es) if err != nil { t.Errorf("TestRequestCount error occurred %v", err) } // Act err = e.Init() if err != nil { t.Errorf("TestRequestCount error occurred %v", err) } e.Start() m.Lock() // Assert create reqCountArr if !reflect.DeepEqual(e.reqCountArr, test.expectedReqArr) { t.Errorf("Expected: %v, Found: %v", test.expectedReqArr, e.reqCountArr) } // Assert sent request count if testing.Short() { // Poor machine's test case assertions are special since they can't run the test fast. totalRecieved := 0 for _, v := range timeReqMap { totalRecieved += v } expected := arraySum(test.expectedReqArr) if totalRecieved != expected { t.Errorf("Poor Machine Expected: %v, Received: %v", totalRecieved, expected) } } else { for i, v := range test.expectedReqArr { if timeReqMap[i] > v+test.delta || timeReqMap[i] < v-test.delta { t.Errorf("Expected: %v, Received: %v, Tick: %v", v, timeReqMap[i], i) } } } m.Unlock() }) } } func TestRequestData(t *testing.T) { t.Parallel() var uri, header1, header2, body, protocol, method string // Test server handler := func(w http.ResponseWriter, r *http.Request) { protocol = r.Proto method = r.Method uri = r.RequestURI header1 = r.Header.Get("Test1") header2 = r.Header.Get("Test2") bodyByte, _ := ioutil.ReadAll(r.Body) body = string(bodyByte) } server := httptest.NewServer(http.HandlerFunc(handler)) defer server.Close() // Prepare h := newDummyHammer() h.Scenario.Steps[0] = types.ScenarioStep{ ID: 1, Method: "GET", URL: server.URL + "/get_test_data", Headers: map[string]string{"Test1": "Test1Value", "Test2": "Test2Value"}, Payload: "Body content", } // Act es, err := InitEngineServices(h) e, err := NewEngine(context.TODO(), h, es) if err != nil { t.Errorf("TestRequestData error occurred %v", err) } err = e.Init() if err != nil { t.Errorf("TestRequestData error occurred %v", err) } e.Start() // Assert if uri != "/get_test_data" { t.Errorf("invalid uri received: %s", uri) } if protocol != "HTTP/1.1" { t.Errorf("invalid protocol received: %v", protocol) } if method != "GET" { t.Errorf("invalid method received: %v", method) } if header1 != "Test1Value" { t.Errorf("invalid header1 received: %s", header1) } if header2 != "Test2Value" { t.Errorf("invalid header2 received: %s", header2) } if body != "Body content" { t.Errorf("invalid body received: %v", body) } } func TestRequestDataForMultiScenarioStep(t *testing.T) { t.Parallel() var uri, header, body, protocol, method []string var m sync.Mutex // Test server handler := func(w http.ResponseWriter, r *http.Request) { m.Lock() protocol = append(protocol, r.Proto) method = append(method, r.Method) uri = append(uri, r.RequestURI) header = append(header, r.Header.Get("Test")) bodyByte, _ := ioutil.ReadAll(r.Body) body = append(body, string(bodyByte)) m.Unlock() } server := httptest.NewServer(http.HandlerFunc(handler)) defer server.Close() // Prepare h := newDummyHammer() h.Scenario = types.Scenario{ Steps: []types.ScenarioStep{ { ID: 1, Method: "GET", URL: server.URL + "/api_get", Headers: map[string]string{"Test": "h1"}, Payload: "Body 1", }, { ID: 2, Method: "POST", URL: server.URL + "/api_post", Headers: map[string]string{"Test": "h2"}, Payload: "Body 2", }, }} // Act es, err := InitEngineServices(h) e, err := NewEngine(context.TODO(), h, es) if err != nil { t.Errorf("TestRequestDataForMultiScenarioStep error occurred %v", err) } err = e.Init() if err != nil { t.Errorf("TestRequestDataForMultiScenarioStep error occurred %v", err) } e.Start() // Assert expected := []string{"/api_get", "/api_post"} if !reflect.DeepEqual(uri, expected) { t.Logf("%#v - %#v", uri, expected) t.Errorf("invalid uri received: %#v expected %#v", uri, expected) } expected = []string{"HTTP/1.1", "HTTP/1.1"} if !reflect.DeepEqual(protocol, expected) { t.Errorf("invalid protocol received: %#v expected %#v", protocol, expected) } expected = []string{"GET", "POST"} if !reflect.DeepEqual(method, expected) { t.Errorf("invalid method received: %#v expected %#v", method, expected) } expected = []string{"h1", "h2"} if !reflect.DeepEqual(header, expected) { t.Errorf("invalid header received: %#v expected %#v", header, expected) } expected = []string{"Body 1", "Body 2"} if !reflect.DeepEqual(body, expected) { t.Errorf("invalid body received: %#v expected %#v", body, expected) } } func TestRequestTimeout(t *testing.T) { t.Parallel() // Prepare tests := []struct { name string timeout int expected bool }{ {"Timeout", 1, false}, {"NotTimeout", 3, true}, } // Act for _, tc := range tests { test := tc t.Run(test.name, func(t *testing.T) { t.Parallel() result := false var m sync.Mutex // Test server handler := func(w http.ResponseWriter, r *http.Request) { time.Sleep(time.Duration(2) * time.Second) m.Lock() result = true m.Unlock() } server := httptest.NewServer(http.HandlerFunc(handler)) defer server.Close() h := newDummyHammer() h.Scenario.Steps[0].Timeout = test.timeout h.Scenario.Steps[0].URL = server.URL es, err := InitEngineServices(h) e, err := NewEngine(context.TODO(), h, es) if err != nil { t.Errorf("TestRequestTimeout error occurred %v", err) } err = e.Init() if err != nil { t.Errorf("TestRequestTimeout error occurred %v", err) } e.Start() // Assert m.Lock() if result != test.expected { t.Errorf("Expected %v, Found :%v", test.expected, result) } m.Unlock() }) } } func TestEngineResult(t *testing.T) { t.Parallel() // Prepare tests := []struct { name string cancelCtx bool expectedStatus string testFailed bool }{ {"CtxCancel", true, "stopped", false}, {"Normal", false, "done", false}, {"Abort", false, "aborted", true}, } // Act for _, tc := range tests { test := tc t.Run(test.name, func(t *testing.T) { t.Parallel() var m sync.Mutex // Test server handler := func(w http.ResponseWriter, r *http.Request) { return } server := httptest.NewServer(http.HandlerFunc(handler)) defer server.Close() h := newDummyHammer() h.TestDuration = 2 h.Scenario.Steps[0].URL = server.URL ctx, cancel := context.WithCancel(context.Background()) if test.name == "Abort" { h.Assertions = map[string]types.TestAssertionOpt{ "false": { // rule evaluated to false Abort: true, Delay: 1, }, } } es, err := InitEngineServices(h) e, err := NewEngine(ctx, h, es) if err != nil { t.Errorf("TestRequestTimeout error occurred %v", err) } err = e.Init() if err != nil { t.Errorf("TestRequestTimeout error occurred %v", err) } if test.cancelCtx { time.AfterFunc(time.Duration(500)*time.Millisecond, func() { cancel() }) } res := e.Start() cancel() // Assert m.Lock() if res != test.expectedStatus { t.Errorf("Expected %v, Found %v", test.expectedStatus, res) } if test.testFailed != e.IsTestFailed() { t.Errorf("Expected %v, Found %v", test.testFailed, e.IsTestFailed()) } m.Unlock() }) } } func TestDynamicData(t *testing.T) { t.Parallel() var headers http.Header var body, uri string // Test server handler := func(w http.ResponseWriter, r *http.Request) { headers = r.Header uri = r.RequestURI bodyByte, _ := ioutil.ReadAll(r.Body) body = string(bodyByte) } server := httptest.NewServer(http.HandlerFunc(handler)) defer server.Close() // Prepare h := newDummyHammer() h.Scenario.Steps[0] = types.ScenarioStep{ ID: 1, Method: "GET", URL: server.URL + "/get_test_data/{{_randomInt}}", Headers: map[string]string{ "Test1": "{{_randomInt}}", "{{_randomInt}}": "Test2Value", "{{_randomColor}}": "{{_randomInt}}", "Test4": "Test4Value", }, Payload: "{{_randomJobArea}}", Auth: types.Auth{ Type: types.AuthHttpBasic, Username: "testuser", Password: "{{_randomBankAccountBic}}", }, } // Act es, err := InitEngineServices(h) e, err := NewEngine(context.TODO(), h, es) if err != nil { t.Errorf("TestRequestData error occurred %v", err) } err = e.Init() if err != nil { t.Errorf("TestRequestData error occurred %v", err) } e.Start() // Assert if i, err := strconv.Atoi(headers.Get("Test1")); err != nil { t.Errorf("invalid header received: %v", i) } if headers.Get("Test4") != "Test4Value" { t.Errorf("invalid header received: %v", headers.Get("Test4")) } for k, v := range headers { vFirst := v[0] if vFirst == "Test2Value" { if i, err := strconv.Atoi(k); err != nil { t.Errorf("invalid header received: %v", i) } } fmt.Println(k, v) } // body contains := false for _, v := range faker.JobAreas { if body == v { contains = true break } } if contains == false { t.Errorf("invalid body received: %v", body) } // basic auth authHeader := strings.ReplaceAll(headers.Get("Authorization"), "Basic ", "") d, _ := base64.StdEncoding.DecodeString(authHeader) usernamePassword := string(d) usernamePasswordSlice := strings.Split(usernamePassword, ":") username := usernamePasswordSlice[0] password := usernamePasswordSlice[1] if username != "testuser" { t.Errorf("invalid username received: %v", username) } contains = false for _, v := range faker.BankAccountBics { if password == v { contains = true break } } if contains == false { t.Errorf("invalid body received: %v", body) } // uri uriDynamicPart := strings.ReplaceAll(uri, "/get_test_data/", "") if i, err := strconv.Atoi(uriDynamicPart); err != nil { t.Errorf("invalid uri received: %v", i) } } func TestGlobalEnvs(t *testing.T) { t.Parallel() // Test server requestCalled := false headerKey := "HEADER_KEY" var gotHeaderVal string handler := func(w http.ResponseWriter, r *http.Request) { requestCalled = true gotHeaderVal = r.Header.Get(headerKey) } path := "/xxx" mux := http.NewServeMux() mux.HandleFunc(path, handler) server := httptest.NewServer(mux) defer server.Close() // Prepare h := newDummyHammer() h.Debug = true h.Scenario.Envs = map[string]interface{}{ "URL_PATH": path, "HEADER_VAL": "headerValToBeInjected", } h.Scenario.Steps[0] = types.ScenarioStep{ ID: 1, Method: "GET", URL: server.URL + "{{URL_PATH}}", Headers: map[string]string{ "HEADER_KEY": "{{HEADER_VAL}}", }, Payload: "{{_randomJobArea}}{{_randomInt}}{{_randomBoolean}}", Auth: types.Auth{ Type: types.AuthHttpBasic, Username: "testuser", Password: "{{_randomBankAccountBic}}", }, } // Act es, err := InitEngineServices(h) e, err := NewEngine(context.TODO(), h, es) if err != nil { t.Errorf("TestGlobalAndCapturedVars error occurred %v", err) } err = e.Init() if err != nil { t.Errorf("TestGlobalAndCapturedVars error occurred %v", err) } e.Start() if !requestCalled { t.Errorf("TestGlobalAndCapturedVars test server has not been called, url path injection failed") } expectedHeaderVal := h.Scenario.Envs["HEADER_VAL"].(string) if !strings.EqualFold(gotHeaderVal, expectedHeaderVal) { t.Errorf("TestGlobalAndCapturedVars header val could not be set from envs, expected : %s, got: %s", expectedHeaderVal, gotHeaderVal) } } func TestInjectEnvToBasicAuth(t *testing.T) { t.Parallel() // Test server requestCalled := false headerKey := "Authorization" var gotHeaderVal string handler := func(w http.ResponseWriter, r *http.Request) { requestCalled = true gotHeaderVal = r.Header.Get(headerKey) } path := "/xxx" mux := http.NewServeMux() mux.HandleFunc(path, handler) server := httptest.NewServer(mux) defer server.Close() // Prepare h := newDummyHammer() h.Debug = true h.Scenario.Envs = map[string]interface{}{ "URL_PATH": path, } h.Scenario.Steps[0] = types.ScenarioStep{ ID: 1, Method: "GET", URL: server.URL + "{{URL_PATH}}", Headers: map[string]string{}, Auth: types.Auth{ Type: types.AuthHttpBasic, Username: "kfc", Password: "1234", }, } // Act es, err := InitEngineServices(h) e, err := NewEngine(context.TODO(), h, es) if err != nil { t.Errorf("TestInjectEnvToBasicAuth error occurred %v", err) } err = e.Init() if err != nil { t.Errorf("TestInjectEnvToBasicAuth error occurred %v", err) } e.Start() if !requestCalled { t.Errorf("TestInjectEnvToBasicAuth test server has not been called, url path injection failed") } // base64 encoding of kfc:1234 -> a2ZjOjEyMzQ= expectedAuthzHeader := "Basic a2ZjOjEyMzQ=" if !strings.EqualFold(gotHeaderVal, expectedAuthzHeader) { t.Errorf("TestInjectEnvToBasicAuth header val could not be set from envs, expected : %s, got: %s", expectedAuthzHeader, gotHeaderVal) } } func TestCapturedEnvsFromJsonBody(t *testing.T) { t.Parallel() // Test server firstRequestCalled := false secondRequestCalled := false headerKey := "HEADER_KEY" var gotHeaderVal string secondReqBody := make(map[string]interface{}, 0) firstReqHandler := func(w http.ResponseWriter, r *http.Request) { firstRequestCalled = true body := struct { Num int `json:"num"` Name string `json:"name"` Champion bool `json:"isChampion"` Squad struct { Results map[string]string `json:"results"` Players []string `json:"players"` } `json:"squad"` }{ Num: 25, Name: "Argentina", Champion: true, Squad: struct { Results map[string]string `json:"results"` Players []string "json:\"players\"" }{ Results: map[string]string{"SAR": "1-2", "MEX": "2-1", "POL": "2-0", "AUS": "2-0", "HOL": "4-2", "CRO": "2-0", "FRA": "CHAMPIONS", }, Players: []string{"messi", "alvarez", "dimaria", "enzo"}, }, } w.Header().Set("Argentina", "Messi") byteBody, _ := json.Marshal(body) w.Write(byteBody) } secondReqHandler := func(w http.ResponseWriter, r *http.Request) { secondRequestCalled = true gotHeaderVal = r.Header.Get(headerKey) bBody, _ := io.ReadAll(r.Body) json.Unmarshal(bBody, &secondReqBody) } pathFirst := "/json-body" pathSecond := "/passed-captured-vars" mux := http.NewServeMux() mux.HandleFunc(pathFirst, firstReqHandler) mux.HandleFunc(pathSecond, secondReqHandler) server := httptest.NewServer(mux) defer server.Close() // Prepare h := newDummyHammer() h.Scenario.Envs = map[string]interface{}{ "FIRST_REQ_URL_PATH": pathFirst, "HEADER_VAL": "headerValToBeInjected", } h.Scenario.Steps = make([]types.ScenarioStep, 2) jsonPath := "isChampion" h.Scenario.Steps[0] = types.ScenarioStep{ ID: 1, Method: "GET", URL: server.URL + "{{FIRST_REQ_URL_PATH}}", Payload: "{{_randomJobArea}}", Auth: types.Auth{ Type: types.AuthHttpBasic, Username: "testuser", Password: "{{_randomBankAccountBic}}", }, EnvsToCapture: []types.EnvCaptureConf{ {Name: "CHAMPION", From: "body", JsonPath: &jsonPath}, }, } h.Scenario.Steps[1] = types.ScenarioStep{ ID: 2, Method: "GET", URL: server.URL + pathSecond, Headers: map[string]string{ "HEADER_KEY": "{{HEADER_VAL}}", }, Auth: types.Auth{ Type: types.AuthHttpBasic, Username: "testuser", Password: "{{_randomBankAccountBic}}", }, Payload: "{\n \"ARGENTINA\" : \"{{CHAMPION}}\"\n}", // json escaped string, use payload_file instead } // Act es, err := InitEngineServices(h) e, err := NewEngine(context.TODO(), h, es) if err != nil { t.Errorf("TestCapturedEnvsFromJsonBody error occurred %v", err) } err = e.Init() if err != nil { t.Errorf("TestCapturedEnvsFromJsonBody error occurred %v", err) } e.Start() if !firstRequestCalled || !secondRequestCalled { t.Errorf("TestCapturedEnvsFromJsonBody test server has not been called, url path injection failed") } expectedHeaderVal := h.Scenario.Envs["HEADER_VAL"].(string) if !strings.EqualFold(gotHeaderVal, expectedHeaderVal) { t.Errorf("TestCapturedEnvsFromJsonBody header val could not be set from envs, expected : %s, got: %s", expectedHeaderVal, gotHeaderVal) } expectedReqPayloadOnSecondReq := true if secondReqBody["ARGENTINA"].(bool) != expectedReqPayloadOnSecondReq { t.Errorf("TestCapturedEnvsFromJsonBody second req body could not be set from envs, expected : %t, got: %s", expectedReqPayloadOnSecondReq, secondReqBody) } } func TestContinueTestOnCaptureError(t *testing.T) { t.Parallel() // Test server firstRequestCalled := false secondRequestCalled := false notExistHeaderKey := "NO_HEADER_KEY" var gotHeaderVal string secondReqBody := make(map[string]interface{}, 0) secondReqInjectedHeaderKey := "INJECTED_HEADER" firstReqHandler := func(w http.ResponseWriter, r *http.Request) { firstRequestCalled = true w.Header().Set("Argentina", "Messi") } secondReqHandler := func(w http.ResponseWriter, r *http.Request) { secondRequestCalled = true gotHeaderVal = r.Header.Get(secondReqInjectedHeaderKey) bBody, _ := io.ReadAll(r.Body) json.Unmarshal(bBody, &secondReqBody) } pathFirst := "/header-capture" pathSecond := "/passed-captured-vars" mux := http.NewServeMux() mux.HandleFunc(pathFirst, firstReqHandler) mux.HandleFunc(pathSecond, secondReqHandler) server := httptest.NewServer(mux) defer server.Close() // Prepare h := newDummyHammer() h.Scenario.Envs = map[string]interface{}{ "FIRST_REQ_URL_PATH": pathFirst, } h.Scenario.Steps = make([]types.ScenarioStep, 2) h.Scenario.Steps[0] = types.ScenarioStep{ ID: 1, Method: "GET", URL: server.URL + "{{FIRST_REQ_URL_PATH}}", EnvsToCapture: []types.EnvCaptureConf{ {Name: "HEADER_VAL", From: "header", Key: ¬ExistHeaderKey}, }, } h.Scenario.Steps[1] = types.ScenarioStep{ ID: 2, Method: "GET", URL: server.URL + pathSecond, Headers: map[string]string{ "INJECTED_HEADER": "{{HEADER_VAL}}", }, } // Act es, err := InitEngineServices(h) e, err := NewEngine(context.TODO(), h, es) if err != nil { t.Errorf("TestContinueTestOnCaptureError error occurred %v", err) } err = e.Init() if err != nil { t.Errorf("TestContinueTestOnCaptureError error occurred %v", err) } e.Start() if !firstRequestCalled || !secondRequestCalled { t.Errorf("TestContinueTestOnCaptureError test server has not been called, url path injection failed") } expectedHeaderVal := "" if !strings.EqualFold(gotHeaderVal, expectedHeaderVal) { // default value "" t.Errorf("TestContinueTestOnCaptureError header val could not be set from envs, must be default value, expected : %s, got: %s", expectedHeaderVal, gotHeaderVal) } } func TestCaptureAndInjectEnvironmentsJsonPayload(t *testing.T) { t.Parallel() firstRequestCalled := false secondRequestCalled := false secondReqBody := make(map[string]interface{}, 0) var secondReqboolHeader string var secondReqnumHeader string firstReqHandler := func(w http.ResponseWriter, r *http.Request) { firstRequestCalled = true body := struct { Num int `json:"num"` Name string `json:"name"` Champion bool `json:"isChampion"` Squad struct { Results map[string]string `json:"results"` Players []string `json:"players"` } `json:"squad"` }{ Num: 25, Name: "Argentina", Champion: true, Squad: struct { Results map[string]string `json:"results"` Players []string "json:\"players\"" }{ Results: map[string]string{"SAR": "1-2", "MEX": "2-1", "POL": "2-0", "AUS": "2-0", "HOL": "4-2", "CRO": "2-0", "FRA": "CHAMPIONS", }, Players: []string{"messi", "alvarez", "dimaria", "enzo"}, }, } w.Header().Set("Argentina", "Messi") w.Header().Set("Content-Type", "application/json") byteBody, _ := json.Marshal(body) w.Write(byteBody) } secondReqHandler := func(w http.ResponseWriter, r *http.Request) { secondRequestCalled = true bBody, _ := io.ReadAll(r.Body) json.Unmarshal(bBody, &secondReqBody) secondReqnumHeader = r.Header.Get("num") secondReqboolHeader = r.Header.Get("bool") } pathFirst := "/header-capture" pathSecond := "/passed-captured-vars" mux := http.NewServeMux() mux.HandleFunc(pathFirst, firstReqHandler) mux.HandleFunc(pathSecond, secondReqHandler) server := httptest.NewServer(mux) defer server.Close() // read config, create hammer configPath := "../config/config_testdata/config_inject_json.json" f, err := os.Open(configPath) if err != nil { t.Errorf("could not open test config %v", err) } byteValue, err := ioutil.ReadAll(f) if err != nil { t.Errorf("could not read test config %v", err) } c, err := config.NewConfigReader(byteValue, config.ConfigTypeJson) if err != nil { t.Errorf("could not create json config reader %v", err) } h, err := c.CreateHammer() if err != nil { t.Errorf("could not create hammer, %v", err) } // set test servers paths h.Scenario.Steps[0].URL = server.URL + pathFirst h.Scenario.Steps[1].URL = server.URL + pathSecond // run engine es, err := InitEngineServices(h) e, err := NewEngine(context.TODO(), h, es) if err != nil { t.Errorf("TestCaptureAndInjectEnvironmentsJsonPayload error occurred %v", err) } err = e.Init() if err != nil { t.Errorf("TestCaptureAndInjectEnvironmentsJsonPayload error occurred %v", err) } e.Start() // assert if !firstRequestCalled || !secondRequestCalled { t.Errorf("TestCaptureAndInjectEnvironmentsJsonPayload test server has not been called, url path injection failed") } if _, ok := secondReqBody["boolField"].(bool); !ok { t.Errorf("TestCaptureAndInjectEnvironmentsJsonPayload bool field could not be injected to json payload") } if _, ok := secondReqBody["numField"].(float64); !ok { t.Errorf("TestCaptureAndInjectEnvironmentsJsonPayload num field could not be injected to json payload") } if _, ok := secondReqBody["strField"].(string); !ok { t.Errorf("TestCaptureAndInjectEnvironmentsJsonPayload string field could not be injected to json payload") } for _, v := range secondReqBody["numArrayField"].([]interface{}) { if _, ok := v.(float64); !ok { t.Errorf("TestCaptureAndInjectEnvironmentsJsonPayload num array field could not be injected to json payload") } } for _, v := range secondReqBody["strArrayField"].([]interface{}) { if _, ok := v.(string); !ok { t.Errorf("TestCaptureAndInjectEnvironmentsJsonPayload str array field could not be injected to json payload") } } obj, _ := secondReqBody["obj"].(map[string]interface{}) if _, ok := obj["objectField"].(map[string]interface{}); !ok { t.Errorf("TestCaptureAndInjectEnvironmentsJsonPayload object field could not be injected to json payload") } if _, ok := obj["arrayField"].([]interface{}); !ok { t.Errorf("TestCaptureAndInjectEnvironmentsJsonPayload array field could not be injected to json payload") } if secondReqnumHeader != "25" { t.Errorf("TestCaptureAndInjectEnvironmentsJsonPayload num header could not be injected to json payload") } if secondReqboolHeader != "true" { t.Errorf("TestCaptureAndInjectEnvironmentsJsonPayload bool header could not be injected to json payload") } } func TestCaptureAndInjectEnvironmentsJsonPayloadDynamic(t *testing.T) { t.Parallel() firstRequestCalled := false secondRequestCalled := false secondReqBody := make(map[string]interface{}, 0) var secondReqboolHeader string var secondReqnumHeader string firstReqHandler := func(w http.ResponseWriter, r *http.Request) { firstRequestCalled = true body := struct { Num int `json:"num"` Name string `json:"name"` Champion bool `json:"isChampion"` Squad struct { Results map[string]string `json:"results"` Players []string `json:"players"` } `json:"squad"` }{ Num: 25, Name: "Argentina", Champion: true, Squad: struct { Results map[string]string `json:"results"` Players []string "json:\"players\"" }{ Results: map[string]string{"SAR": "1-2", "MEX": "2-1", "POL": "2-0", "AUS": "2-0", "HOL": "4-2", "CRO": "2-0", "FRA": "CHAMPIONS", }, Players: []string{"messi", "alvarez", "dimaria", "enzo"}, }, } w.Header().Set("Argentina", "Messi") w.Header().Set("Content-Type", "application/json") byteBody, _ := json.Marshal(body) w.Write(byteBody) } secondReqHandler := func(w http.ResponseWriter, r *http.Request) { secondRequestCalled = true bBody, _ := io.ReadAll(r.Body) json.Unmarshal(bBody, &secondReqBody) secondReqnumHeader = r.Header.Get("num") secondReqboolHeader = r.Header.Get("bool") } pathFirst := "/header-capture" pathSecond := "/passed-captured-vars" mux := http.NewServeMux() mux.HandleFunc(pathFirst, firstReqHandler) mux.HandleFunc(pathSecond, secondReqHandler) server := httptest.NewServer(mux) defer server.Close() // read config, create hammer configPath := "../config/config_testdata/config_inject_json_dynamic.json" f, err := os.Open(configPath) if err != nil { t.Errorf("could not open test config %v", err) } byteValue, err := ioutil.ReadAll(f) if err != nil { t.Errorf("could not read test config %v", err) } c, err := config.NewConfigReader(byteValue, config.ConfigTypeJson) if err != nil { t.Errorf("could not create json config reader %v", err) } h, err := c.CreateHammer() if err != nil { t.Errorf("could not create hammer, %v", err) } // set test servers paths h.Scenario.Steps[0].URL = server.URL + pathFirst h.Scenario.Steps[1].URL = server.URL + pathSecond // run engine es, err := InitEngineServices(h) e, err := NewEngine(context.TODO(), h, es) if err != nil { t.Errorf("TestCaptureAndInjectEnvironmentsJsonPayloadDynamic error occurred %v", err) } err = e.Init() if err != nil { t.Errorf("TestCaptureAndInjectEnvironmentsJsonPayloadDynamic error occurred %v", err) } e.Start() // assert if !firstRequestCalled || !secondRequestCalled { t.Errorf("TestCaptureAndInjectEnvironmentsJsonPayloadDynamic test server has not been called, url path injection failed") } if _, ok := secondReqBody["name"].(string); !ok { t.Errorf("TestCaptureAndInjectEnvironmentsJsonPayloadDynamic bool field could not be injected to json payload") } if _, ok := secondReqBody["city"].(string); !ok { t.Errorf("TestCaptureAndInjectEnvironmentsJsonPayloadDynamic num field could not be injected to json payload") } if _, ok := secondReqBody["age"].(float64); !ok { t.Errorf("TestCaptureAndInjectEnvironmentsJsonPayloadDynamic string field could not be injected to json payload") } if secondReqnumHeader != "25" { t.Errorf("TestCaptureAndInjectEnvironmentsJsonPayloadDynamic num header could not be injected to json payload") } if secondReqboolHeader != "true" { t.Errorf("TestCaptureAndInjectEnvironmentsJsonPayloadDynamic bool header could not be injected to json payload") } } func TestEnvInjectToXmlPayload(t *testing.T) { t.Parallel() requestCalled := false readReqBody := make([]byte, 0) injectedEnv := "hello" expectedReqBody := []byte( fmt.Sprintf(` %s `, injectedEnv)) firstReqHandler := func(w http.ResponseWriter, r *http.Request) { requestCalled = true readReqBody, _ = io.ReadAll(r.Body) } pathFirst := "/header-capture" mux := http.NewServeMux() mux.HandleFunc(pathFirst, firstReqHandler) server := httptest.NewServer(mux) defer server.Close() // read config, create hammer configPath := "../config/config_testdata/config_inject_xml.json" f, err := os.Open(configPath) if err != nil { t.Errorf("could not open test config %v", err) } byteValue, err := ioutil.ReadAll(f) if err != nil { t.Errorf("could not read test config %v", err) } c, err := config.NewConfigReader(byteValue, config.ConfigTypeJson) if err != nil { t.Errorf("could not create json config reader %v", err) } h, err := c.CreateHammer() if err != nil { t.Errorf("could not create hammer, %v", err) } // set test servers paths h.Scenario.Steps[0].URL = server.URL + pathFirst // run engine es, err := InitEngineServices(h) e, err := NewEngine(context.TODO(), h, es) if err != nil { t.Errorf("TestInjectXmlPayload error occurred %v", err) } err = e.Init() if err != nil { t.Errorf("TestInjectXmlPayload error occurred %v", err) } e.Start() // assert if !requestCalled { t.Errorf("TestInjectXmlPayload test server has not been called, url path injection failed") } if bytes.Equal(readReqBody, expectedReqBody) { } } func TestCaptureHeaderWithRegex(t *testing.T) { t.Parallel() // Test server firstRequestCalled := false secondRequestCalled := false headerKey := "Argentina" var gotHeaderVal string secondReqBody := make(map[string]interface{}, 0) secondReqInjectedHeaderKey := "BallondorWinner" firstReqHandler := func(w http.ResponseWriter, r *http.Request) { firstRequestCalled = true w.Header().Set(headerKey, "messi_10alvarez9") } secondReqHandler := func(w http.ResponseWriter, r *http.Request) { secondRequestCalled = true gotHeaderVal = r.Header.Get(secondReqInjectedHeaderKey) bBody, _ := io.ReadAll(r.Body) json.Unmarshal(bBody, &secondReqBody) } pathFirst := "/header-capture" pathSecond := "/passed-captured-vars" mux := http.NewServeMux() mux.HandleFunc(pathFirst, firstReqHandler) mux.HandleFunc(pathSecond, secondReqHandler) server := httptest.NewServer(mux) defer server.Close() // Prepare h := newDummyHammer() h.Scenario.Envs = map[string]interface{}{ "FIRST_REQ_URL_PATH": pathFirst, } h.Scenario.Steps = make([]types.ScenarioStep, 2) regex := "[a-z]+_[0-9]+" h.Scenario.Steps[0] = types.ScenarioStep{ ID: 1, Method: "GET", URL: server.URL + "{{FIRST_REQ_URL_PATH}}", EnvsToCapture: []types.EnvCaptureConf{ {Name: "GOAT", From: "header", Key: &headerKey, RegExp: &types.RegexCaptureConf{Exp: ®ex, No: 0}}, }, } h.Scenario.Steps[1] = types.ScenarioStep{ ID: 2, Method: "GET", URL: server.URL + pathSecond, Headers: map[string]string{ secondReqInjectedHeaderKey: "{{GOAT}}", }, } // Act es, err := InitEngineServices(h) e, err := NewEngine(context.TODO(), h, es) if err != nil { t.Errorf("TestCaptureHeaderWithRegex error occurred %v", err) } err = e.Init() if err != nil { t.Errorf("TestCaptureHeaderWithRegex error occurred %v", err) } e.Start() if !firstRequestCalled || !secondRequestCalled { t.Errorf("TestCaptureHeaderWithRegex test server has not been called, url path injection failed") } expectedHeaderVal := "messi_10" if !strings.EqualFold(gotHeaderVal, expectedHeaderVal) { t.Errorf( "TestCaptureHeaderWithRegex header val could not be set from envs, must be default value, expected : %s, got: %s", expectedHeaderVal, gotHeaderVal) } } func TestCaptureCookie(t *testing.T) { t.Parallel() // Test server firstRequestCalled := false secondRequestCalled := false cookieName := "Argentina" var gotCookieVal string secondReqBody := make(map[string]interface{}, 0) secondReqInjectedHeaderKey := "BallondorWinner" expectedCookieValue := "messi_10" firstReqHandler := func(w http.ResponseWriter, r *http.Request) { firstRequestCalled = true http.SetCookie(w, &http.Cookie{Name: cookieName, Value: expectedCookieValue}) } secondReqHandler := func(w http.ResponseWriter, r *http.Request) { secondRequestCalled = true gotCookieVal = r.Header.Get(secondReqInjectedHeaderKey) bBody, _ := io.ReadAll(r.Body) json.Unmarshal(bBody, &secondReqBody) } pathFirst := "/header-capture" pathSecond := "/passed-captured-vars" mux := http.NewServeMux() mux.HandleFunc(pathFirst, firstReqHandler) mux.HandleFunc(pathSecond, secondReqHandler) server := httptest.NewServer(mux) defer server.Close() // Prepare h := newDummyHammer() h.Scenario.Envs = map[string]interface{}{ "FIRST_REQ_URL_PATH": pathFirst, } h.Scenario.Steps = make([]types.ScenarioStep, 2) h.Scenario.Steps[0] = types.ScenarioStep{ ID: 1, Method: "GET", URL: server.URL + "{{FIRST_REQ_URL_PATH}}", EnvsToCapture: []types.EnvCaptureConf{ {Name: "GOAT", From: "cookies", CookieName: &cookieName}, }, } h.Scenario.Steps[1] = types.ScenarioStep{ ID: 2, Method: "GET", URL: server.URL + pathSecond, Headers: map[string]string{ secondReqInjectedHeaderKey: "{{GOAT}}", }, } // Act es, err := InitEngineServices(h) e, err := NewEngine(context.TODO(), h, es) if err != nil { t.Errorf("TestCaptureCookie error occurred %v", err) } err = e.Init() if err != nil { t.Errorf("TestCaptureCookie error occurred %v", err) } e.Start() if !firstRequestCalled || !secondRequestCalled { t.Errorf("TestCaptureCookie test server has not been called, url path injection failed") } if !strings.EqualFold(gotCookieVal, expectedCookieValue) { t.Errorf( "TestCaptureCookie, expected : %s, got: %s", expectedCookieValue, gotCookieVal) } } func TestCaptureStringPayloadWithRegex(t *testing.T) { t.Parallel() // Test server firstRequestCalled := false secondRequestCalled := false var secondReqBody []byte firstReqHandler := func(w http.ResponseWriter, r *http.Request) { firstRequestCalled = true w.Write([]byte("messi_10alvarez9")) } secondReqHandler := func(w http.ResponseWriter, r *http.Request) { secondRequestCalled = true secondReqBody, _ = io.ReadAll(r.Body) } pathFirst := "/header-capture" pathSecond := "/passed-captured-vars" mux := http.NewServeMux() mux.HandleFunc(pathFirst, firstReqHandler) mux.HandleFunc(pathSecond, secondReqHandler) server := httptest.NewServer(mux) defer server.Close() // Prepare h := newDummyHammer() h.Scenario.Envs = map[string]interface{}{ "FIRST_REQ_URL_PATH": pathFirst, } h.Scenario.Steps = make([]types.ScenarioStep, 2) regex := "[a-z]+_[0-9]+" h.Scenario.Steps[0] = types.ScenarioStep{ ID: 1, Method: "GET", URL: server.URL + "{{FIRST_REQ_URL_PATH}}", EnvsToCapture: []types.EnvCaptureConf{ {Name: "GOAT", From: "body", RegExp: &types.RegexCaptureConf{Exp: ®ex, No: 0}}, }, } h.Scenario.Steps[1] = types.ScenarioStep{ ID: 2, Method: "GET", URL: server.URL + pathSecond, Payload: "{{GOAT}}", } // Act es, err := InitEngineServices(h) e, err := NewEngine(context.TODO(), h, es) if err != nil { t.Errorf("TestCaptureHeaderWithRegex error occurred %v", err) } err = e.Init() if err != nil { t.Errorf("TestCaptureHeaderWithRegex error occurred %v", err) } e.Start() if !firstRequestCalled || !secondRequestCalled { t.Errorf("TestCaptureHeaderWithRegex test server has not been called, url path injection failed") } expectedBodyVal := []byte("messi_10") if !bytes.Equal(secondReqBody, expectedBodyVal) { t.Errorf( "TestCaptureHeaderWithRegex header val could not be set from envs, must be default value, expected : %s, got: %s", expectedBodyVal, secondReqBody) } } func TestBothDynamicVarAndEnvVar(t *testing.T) { t.Parallel() // Test server requestCalled := false headerKey := "country" var gotHeaderVal string handler := func(w http.ResponseWriter, r *http.Request) { requestCalled = true gotHeaderVal = r.Header.Get(headerKey) } path := "/xxx" mux := http.NewServeMux() mux.HandleFunc(path, handler) server := httptest.NewServer(mux) defer server.Close() // Prepare h := newDummyHammer() h.Debug = true h.Scenario.Envs = map[string]interface{}{ "URL_PATH": path, "COUNTRY_HEADER_KEY": headerKey, } h.Scenario.Steps[0] = types.ScenarioStep{ ID: 1, Method: "GET", URL: server.URL + "{{URL_PATH}}", Headers: map[string]string{ "{{COUNTRY_HEADER_KEY}}": "{{_randomCountry}}", }, Payload: "{{_randomJobArea}}", Auth: types.Auth{ Type: types.AuthHttpBasic, Username: "testuser", Password: "{{_randomBankAccountBic}}", }, } // Act es, err := InitEngineServices(h) e, err := NewEngine(context.TODO(), h, es) if err != nil { t.Errorf("TestBothDynamicVarAndEnvVar error occurred %v", err) } err = e.Init() if err != nil { t.Errorf("TestBothDynamicVarAndEnvVar error occurred %v", err) } e.Start() if !requestCalled { t.Errorf("TestBothDynamicVarAndEnvVar test server has not been called, url path injection failed") } if strings.EqualFold(gotHeaderVal, "") { t.Errorf("TestBothDynamicVarAndEnvVar dynamic var could not be set, expected a country, got: %s", "") } } func TestDynamicVarAndEnvVarInSameSection(t *testing.T) { t.Parallel() // Test server requestCalled := false headerKey := "composite" var gotHeaderVal string handler := func(w http.ResponseWriter, r *http.Request) { requestCalled = true gotHeaderVal = r.Header.Get(headerKey) } path := "/xxx" mux := http.NewServeMux() mux.HandleFunc(path, handler) server := httptest.NewServer(mux) defer server.Close() // Prepare h := newDummyHammer() h.Debug = true h.Scenario.Envs = map[string]interface{}{ "A": "B", "URL_PATH": path, "COMPOSITE_KEY": headerKey, } h.Scenario.Steps[0] = types.ScenarioStep{ ID: 1, Method: "GET", URL: server.URL + "{{URL_PATH}}", Headers: map[string]string{ "{{COMPOSITE_KEY}}": "{{_randomBoolean}}-{{A}}", }, } // Act es, err := InitEngineServices(h) e, err := NewEngine(context.TODO(), h, es) if err != nil { t.Errorf("TestDynamicVarAndEnvVarInSameSection error occurred %v", err) } err = e.Init() if err != nil { t.Errorf("TestDynamicVarAndEnvVarInSameSection error occurred %v", err) } e.Start() if !requestCalled { t.Errorf("TestDynamicVarAndEnvVarInSameSection test server has not been called, url path injection failed") } re := regexp.MustCompile("(true|false|)-B") if !re.MatchString(gotHeaderVal) { t.Errorf("TestDynamicVarAndEnvVarInSameSection gotHeaderVal did not match expected regex, got: %s", gotHeaderVal) } } func TestLoadRandomInfoFromData(t *testing.T) { t.Parallel() // Test server requestCalled := false kenan := "Kenan" fatih := "Fatih" expectedKenanAge := "25" expectedFatihAge := "29" ageMap := map[string]string{kenan: "", fatih: ""} handler := func(w http.ResponseWriter, r *http.Request) { requestCalled = true kenanAge := r.Header.Get(kenan) fatihAge := r.Header.Get(fatih) if kenanAge != "" { ageMap[kenan] = kenanAge } if fatihAge != "" { ageMap[fatih] = fatihAge } } path := "/xxx" mux := http.NewServeMux() mux.HandleFunc(path, handler) server := httptest.NewServer(mux) defer server.Close() // Prepare h := newDummyHammer() var csvData types.CsvData csvData.Random = false csvData.Rows = []map[string]interface{}{{ "name": kenan, "age": expectedKenanAge, }, { "name": fatih, "age": expectedFatihAge, }} h.Scenario.Data = map[string]types.CsvData{"info": csvData} h.Scenario.Envs = map[string]interface{}{ "A": "B", "URL_PATH": path, } h.TestDataConf = map[string]types.CsvConf{ "info": { Path: path, Delimiter: "", SkipFirstLine: false, Vars: map[string]types.Tag{ "0": { Tag: "name", Type: "string", }, "1": { Tag: "age", Type: "string", }, }, SkipEmptyLine: false, AllowQuota: false, Order: "", }, } h.IterationCount = 2 h.Scenario.Steps[0] = types.ScenarioStep{ ID: 1, Method: "GET", URL: server.URL + "{{URL_PATH}}", Headers: map[string]string{ "{{data.info.name}}": "{{data.info.age}}", }, } // Act es, err := InitEngineServices(h) e, err := NewEngine(context.TODO(), h, es) if err != nil { t.Errorf("TestLoadRandomInfoFromData error occurred %v", err) } originalReadTestData := readTestData readTestData = func(testDataConf map[string]types.CsvConf) (map[string]types.CsvData, error) { return map[string]types.CsvData{"info": csvData}, nil } err = e.Init() if err != nil { t.Errorf("TestLoadRandomInfoFromData error occurred %v", err) } e.Start() readTestData = originalReadTestData if !requestCalled { t.Errorf("TestLoadRandomInfoFromData test server has not been called, url path injection failed") } if ageMap[kenan] != expectedKenanAge || ageMap[fatih] != expectedFatihAge { t.Errorf("TestLoadRandomInfoFromData did not match") } } func TestDataCsv(t *testing.T) { readConfigFile := func(path string) []byte { f, _ := os.Open(path) byteValue, _ := ioutil.ReadAll(f) return byteValue } jsonReader, _ := config.NewConfigReader(readConfigFile("../config/config_testdata/config_data_csv.json"), config.ConfigTypeJson) expectedRandom := true h, _ := jsonReader.CreateHammer() data, err := readTestData(h.TestDataConf) if err != nil { t.Errorf("TestDataCsv error occurred: %v", err) } csvData := data["info"] if !reflect.DeepEqual(csvData.Random, expectedRandom) { t.Errorf("TestCreateHammerDataCsv got: %t expected: %t", csvData.Random, expectedRandom) } expectedRow := map[string]interface{}{ "name": "Kenan", "city": "Tokat", "team": "Galatasaray", "payload": map[string]interface{}{ "data": map[string]interface{}{ "profile": map[string]interface{}{ "name": "Kenan", }, }, }, "age": 25, } if !reflect.DeepEqual(expectedRow, csvData.Rows[0]) { t.Errorf("TestCreateHammerDataCsv got: %#v expected: %#v", csvData.Rows[0], expectedRow) } } func TestInvalidCsvEnvs(t *testing.T) { readConfigFile := func(path string) []byte { f, _ := os.Open(path) byteValue, _ := ioutil.ReadAll(f) return byteValue } jsonReader, _ := config.NewConfigReader(readConfigFile("../config/config_testdata/config_invalid_csv_envs.json"), config.ConfigTypeJson) h, _ := jsonReader.CreateHammer() err := h.Validate() if err == nil { t.Errorf("TestInvalidCsvEnvs should be errored") } } func TestCreateInitialCookiesReturnsErr(t *testing.T) { t.Parallel() // Prepare h := newDummyHammer() h.CookiesEnabled = true h.Cookies = []types.CustomCookie{ {Name: "test", Value: "test"}, } tmpFunc := createInitialCookies createInitialCookies = func(cookies []types.CustomCookie) ([]*http.Cookie, error) { return nil, errors.New("test error") } defer func() { createInitialCookies = tmpFunc }() // Act es, err := InitEngineServices(h) e, err := NewEngine(context.TODO(), h, es) if err != nil { t.Errorf("TestCreateInitialCookiesReturnsErr error occurred %v", err) } err = e.Init() if err == nil { t.Errorf("TestCreateInitialCookiesReturnsErr should be errored") } } func TestCreateInitialCookies(t *testing.T) { readConfigFile := func(path string) []byte { f, _ := os.Open(path) byteValue, _ := ioutil.ReadAll(f) return byteValue } jsonReader, _ := config.NewConfigReader(readConfigFile("../config/config_testdata/config_init_cookies.json"), config.ConfigTypeJson) h, _ := jsonReader.CreateHammer() initCookies, err := createInitialCookies(h.Cookies) if err != nil { t.Errorf("TestCreateInitialCookies error occurred: %v", err) } rawExpires := "Thu, 16 Mar 2023 09:24:02 GMT" expires, _ := time.Parse(time.RFC1123, rawExpires) expectedCookie := http.Cookie{ Name: "platform", Value: "web", Path: "/", Domain: "httpbin.ddosify.com", Expires: expires, RawExpires: rawExpires, MaxAge: 0, Secure: false, HttpOnly: true, SameSite: 0, Raw: "", Unparsed: []string{}, } if !reflect.DeepEqual(expectedCookie, *initCookies[0]) { t.Errorf("TestCreateInitialCookies got: %v expected: %v", initCookies[0], expectedCookie) } } // The test creates a web server with Certificate auth, // then it spawns an Engine and verifies that the auth was successfully passsed. func TestTLSMutualAuth(t *testing.T) { t.Parallel() handlerCalls := 0 // Test server handler := func(w http.ResponseWriter, r *http.Request) { handlerCalls += 1 } server := httptest.NewUnstartedServer(http.HandlerFunc(handler)) defer server.Close() // prepare TLS files cert, certKey := generateCerts() certFile, keyFile, err := createCertPairFiles(cert, certKey) if err != nil { t.Errorf("Failed to prepare certs %v", err) } defer os.Remove(certFile.Name()) defer os.Remove(keyFile.Name()) // Prepare h := newDummyHammer() h.Scenario.Steps[0] = types.ScenarioStep{ ID: 1, Method: "GET", URL: "", } certVal, poolVal, err := types.ParseTLS(certFile.Name(), keyFile.Name()) if err != nil { t.Errorf("Failed to parse certs %v", err) } h.Scenario.Steps[0].Cert = certVal h.Scenario.Steps[0].CertPool = poolVal server.TLS = &tls.Config{ ClientAuth: tls.RequireAndVerifyClientCert, ClientCAs: h.Scenario.Steps[0].CertPool, Certificates: []tls.Certificate{h.Scenario.Steps[0].Cert}, } server.StartTLS() h.Scenario.Steps[0].URL = server.URL // Act es, err := InitEngineServices(h) e, err := NewEngine(context.TODO(), h, es) if err != nil { t.Errorf("TestRequestData error occurred %v", err) } err = e.Init() if err != nil { t.Errorf("TestRequestData error occurred %v", err) } e.Start() // Assert if handlerCalls == 0 { t.Errorf("handler was not called at all: %#v", handlerCalls) } } // The test creates a web server with Certificate auth, // then it spawns an Engine, but the engine doesn't have a certificate therefore it's expected that no handler is called. func TestTLSMutualAuthButWeHaveNoCerts(t *testing.T) { t.Parallel() handlerCalls := 0 // Test server handler := func(w http.ResponseWriter, r *http.Request) { handlerCalls += 1 } server := httptest.NewUnstartedServer(http.HandlerFunc(handler)) defer server.Close() // prepare TLS files cert, certKey := generateCerts() certFile, keyFile, err := createCertPairFiles(cert, certKey) if err != nil { t.Errorf("Failed to prepare certs %v", err) } defer os.Remove(certFile.Name()) defer os.Remove(keyFile.Name()) // Prepare h := newDummyHammer() h.Scenario.Steps[0] = types.ScenarioStep{ ID: 1, Method: "GET", URL: "", } certVal, poolVal, err := types.ParseTLS(certFile.Name(), keyFile.Name()) if err != nil { t.Errorf("Failed to parse certs %v", err) } h.Scenario.Steps[0].Cert = certVal h.Scenario.Steps[0].CertPool = poolVal server.TLS = &tls.Config{ ClientAuth: tls.RequireAndVerifyClientCert, ClientCAs: h.Scenario.Steps[0].CertPool, Certificates: []tls.Certificate{h.Scenario.Steps[0].Cert}, } server.StartTLS() h.Scenario.Steps[0].URL = server.URL // invalidate the certs h.Scenario.Steps[0].CertPool = nil h.Scenario.Steps[0].Cert = tls.Certificate{} es, err := InitEngineServices(h) e, err := NewEngine(context.TODO(), h, es) if err != nil { t.Errorf("TestRequestData error occurred %v", err) } err = e.Init() if err != nil { t.Errorf("TestRequestData error occurred %v", err) } e.Start() if handlerCalls != 0 { t.Errorf("handler was called unexpectedly: %#v", handlerCalls) } } // The test creates a web server with Certificate auth, // then it spawns an Engine, but the engine have a different certificate therefore it's expected that no handler is called. func TestTLSMutualAuthButServerAndClientHasDifferentCerts(t *testing.T) { t.Parallel() handlerCalls := 0 // Test server handler := func(w http.ResponseWriter, r *http.Request) { handlerCalls += 1 } server := httptest.NewUnstartedServer(http.HandlerFunc(handler)) defer server.Close() // prepare TLS files cert, certKey := generateCerts() certFile, keyFile, err := createCertPairFiles(cert, certKey) if err != nil { t.Errorf("Failed to prepare certs %v", err) } defer os.Remove(certFile.Name()) defer os.Remove(keyFile.Name()) // prepare server TLS files cert, certKey = generateCerts2() certFile2, keyFile2, err := createCertPairFiles(cert, certKey) if err != nil { t.Errorf("Failed to prepare certs %v", err) } defer os.Remove(certFile2.Name()) defer os.Remove(keyFile2.Name()) // Prepare h := newDummyHammer() h.Scenario.Steps[0] = types.ScenarioStep{ID: 1, Method: "GET", URL: ""} // here we use server certs first certVal, poolVal, err := types.ParseTLS(certFile.Name(), keyFile.Name()) if err != nil { t.Errorf("Failed to parse certs %v", err) } h.Scenario.Steps[0].Cert = certVal h.Scenario.Steps[0].CertPool = poolVal server.TLS = &tls.Config{ ClientAuth: tls.RequireAndVerifyClientCert, ClientCAs: h.Scenario.Steps[0].CertPool, Certificates: []tls.Certificate{h.Scenario.Steps[0].Cert}, } server.StartTLS() h.Scenario.Steps[0].URL = server.URL // here we use different certs // so the server and client has different pairs certVal, poolVal, err = types.ParseTLS(certFile2.Name(), keyFile2.Name()) if err != nil { t.Errorf("Failed to parse certs %v", err) } h.Scenario.Steps[0].Cert = certVal h.Scenario.Steps[0].CertPool = poolVal es, err := InitEngineServices(h) e, err := NewEngine(context.TODO(), h, es) if err != nil { t.Errorf("TestRequestData error occurred %v", err) } err = e.Init() if err != nil { t.Errorf("TestRequestData error occurred %v", err) } e.Start() if handlerCalls != 0 { t.Errorf("handler was called unexpectedly: %#v", handlerCalls) } } func TestEngineModeUserKeepAlive(t *testing.T) { t.Parallel() // For DistinctUser and RepeatedUser modes // Test server clientAddress1 := []string{} clientAddress2 := []string{} var m1 sync.Mutex var m2 sync.Mutex firstReqHandler := func(w http.ResponseWriter, r *http.Request) { m1.Lock() defer m1.Unlock() clientAddress1 = append(clientAddress1, r.RemoteAddr) // network address that sent the request } secondReqHandler := func(w http.ResponseWriter, r *http.Request) { m2.Lock() defer m2.Unlock() clientAddress2 = append(clientAddress2, r.RemoteAddr) // network address that sent the request } pathFirst := "/first" pathSecond := "/second" mux := http.NewServeMux() mux.HandleFunc(pathFirst, firstReqHandler) mux.HandleFunc(pathSecond, secondReqHandler) host := httptest.NewServer(mux) defer host.Close() // Prepare h := newDummyHammer() h.IterationCount = 2 h.Scenario.Steps = make([]types.ScenarioStep, 2) h.Scenario.Steps[0] = types.ScenarioStep{ ID: 1, Method: "GET", URL: host.URL + pathFirst, } h.Scenario.Steps[1] = types.ScenarioStep{ ID: 2, Method: "GET", URL: host.URL + pathSecond, } // Act es, err := InitEngineServices(h) h.EngineMode = types.EngineModeRepeatedUser // could have been DistinctUser also e, err := NewEngine(context.TODO(), h, es) if err != nil { t.Errorf("TestEngineModeDistinctUserKeepAlive error occurred %v", err) } err = e.Init() if err != nil { t.Errorf("TestEngineModeDistinctUserKeepAlive error occurred %v", err) } e.Start() // same host // check first iter if clientAddress1[0] != clientAddress2[0] { t.Errorf("TestEngineModeDistinctUserKeepAlive, same hosts connection should be same throughout iteration") } // check second iter if clientAddress1[1] != clientAddress2[1] { t.Errorf("TestEngineModeDistinctUserKeepAlive, same hosts connection should be same throughout iteration") } } func TestEngineModeUserKeepAliveDifferentHosts(t *testing.T) { t.Parallel() // For DistinctUser and RepeatedUser modes // Test server clientAddress := make(map[string]struct{}) var m sync.Mutex firstReqHandler := func(w http.ResponseWriter, r *http.Request) { m.Lock() defer m.Unlock() clientAddress[r.RemoteAddr] = struct{}{} // network address that sent the request } pathFirst := "/first" mux := http.NewServeMux() mux.HandleFunc(pathFirst, firstReqHandler) host1 := httptest.NewServer(mux) host2 := httptest.NewServer(mux) defer host1.Close() defer host2.Close() // Prepare h := newDummyHammer() h.IterationCount = 1 h.Scenario.Steps = make([]types.ScenarioStep, 4) h.Scenario.Steps[0] = types.ScenarioStep{ ID: 1, Method: "GET", URL: host1.URL + pathFirst, } h.Scenario.Steps[1] = types.ScenarioStep{ ID: 2, Method: "GET", URL: host1.URL + pathFirst, } h.Scenario.Steps[2] = types.ScenarioStep{ ID: 3, Method: "GET", URL: host2.URL + pathFirst, } h.Scenario.Steps[3] = types.ScenarioStep{ ID: 4, Method: "GET", URL: host2.URL + pathFirst, } // Act es, err := InitEngineServices(h) h.EngineMode = types.EngineModeDistinctUser // could have been RepeatedUser also e, err := NewEngine(context.TODO(), h, es) if err != nil { t.Errorf("TestEngineModeUserKeepAliveDifferentHosts error occurred %v", err) } err = e.Init() if err != nil { t.Errorf("TestEngineModeUserKeepAliveDifferentHosts error occurred %v", err) } e.Start() // one iteration, two hosts, two connections expected if len(clientAddress) != 2 { t.Errorf("TestEngineModeUserKeepAliveDifferentHosts, expected 2 connections, got : %d", len(clientAddress)) } } func TestEngineModeUserKeepAlive_StepsKeepAliveFalse(t *testing.T) { t.Parallel() // For DistinctUser and RepeatedUser modes // Test server clientAddress := make(map[string]struct{}) var m sync.Mutex firstReqHandler := func(w http.ResponseWriter, r *http.Request) { m.Lock() defer m.Unlock() clientAddress[r.RemoteAddr] = struct{}{} // network address that sent the request } pathFirst := "/first" mux := http.NewServeMux() mux.HandleFunc(pathFirst, firstReqHandler) host1 := httptest.NewServer(mux) defer host1.Close() // Prepare h := newDummyHammer() h.IterationCount = 1 h.Scenario.Steps = make([]types.ScenarioStep, 4) // connection opened by 1 will not be reused h.Scenario.Steps[0] = types.ScenarioStep{ ID: 1, Method: "GET", URL: host1.URL + pathFirst, Headers: map[string]string{"Connection": "close"}, } // below will use the connection opened by 2 h.Scenario.Steps[1] = types.ScenarioStep{ ID: 2, Method: "GET", URL: host1.URL + pathFirst, } h.Scenario.Steps[2] = types.ScenarioStep{ ID: 3, Method: "GET", URL: host1.URL + pathFirst, } h.Scenario.Steps[3] = types.ScenarioStep{ ID: 4, Method: "GET", URL: host1.URL + pathFirst, } // Act h.EngineMode = types.EngineModeDistinctUser es, err := InitEngineServices(h) e, err := NewEngine(context.TODO(), h, es) if err != nil { t.Errorf("TestEngineModeUserKeepAliveDifferentHosts error occurred %v", err) } err = e.Init() if err != nil { t.Errorf("TestEngineModeUserKeepAliveDifferentHosts error occurred %v", err) } e.Start() // one iteration, one host, 4 steps, one's keep-alive is false (Connection: close) if len(clientAddress) != 2 { t.Errorf("TestEngineModeUserKeepAliveDifferentHosts, expected 2 connections, got : %d", len(clientAddress)) } } func TestEngineModeDdosifyKeepAlive(t *testing.T) { t.Parallel() // Test server clientAddress1 := []string{} clientAddress2 := []string{} var m1 sync.Mutex var m2 sync.Mutex firstReqHandler := func(w http.ResponseWriter, r *http.Request) { m1.Lock() defer m1.Unlock() clientAddress1 = append(clientAddress1, r.RemoteAddr) // network address that sent the request } secondReqHandler := func(w http.ResponseWriter, r *http.Request) { m2.Lock() defer m2.Unlock() clientAddress2 = append(clientAddress2, r.RemoteAddr) // network address that sent the request } pathFirst := "/first" pathSecond := "/second" mux := http.NewServeMux() mux.HandleFunc(pathFirst, firstReqHandler) mux.HandleFunc(pathSecond, secondReqHandler) host := httptest.NewServer(mux) defer host.Close() // Prepare h := newDummyHammer() h.IterationCount = 2 h.Scenario.Steps = make([]types.ScenarioStep, 2) h.Scenario.Steps[0] = types.ScenarioStep{ ID: 1, Method: "GET", URL: host.URL + pathFirst, } h.Scenario.Steps[1] = types.ScenarioStep{ ID: 2, Method: "GET", URL: host.URL + pathSecond, } // Act h.EngineMode = types.EngineModeDdosify es, err := InitEngineServices(h) e, err := NewEngine(context.TODO(), h, es) if err != nil { t.Errorf("TestEngineModeDdosifyKeepAlive error occurred %v", err) } err = e.Init() if err != nil { t.Errorf("TestEngineModeDdosifyKeepAlive error occurred %v", err) } e.Start() // same host // in ddosify mode every step has its own client, therefore connections should be different // check first iter if clientAddress1[0] == clientAddress2[0] { t.Errorf("TestEngineModeDistinctUserKeepAlive, ") } // check second iter if clientAddress1[1] == clientAddress2[1] { t.Errorf("TestEngineModeDistinctUserKeepAlive, ") } } func createCertPairFiles(cert string, certKey string) (*os.File, *os.File, error) { certFile, err := os.CreateTemp("", ".pem") if err != nil { return nil, nil, err } _, err = io.WriteString(certFile, cert) if err != nil { return nil, nil, err } keyFile, err := os.CreateTemp("", ".pem") if err != nil { return nil, nil, err } _, err = io.WriteString(keyFile, certKey) if err != nil { return nil, nil, err } return certFile, keyFile, nil } func generateCerts() (string, string) { cert := `-----BEGIN CERTIFICATE----- MIIDazCCAlOgAwIBAgIUS4UhTks8aRCQ1k9IGn437ZyP3MgwDQYJKoZIhvcNAQEL BQAwRTELMAkGA1UEBhMCQVUxEzARBgNVBAgMClNvbWUtU3RhdGUxITAfBgNVBAoM GEludGVybmV0IFdpZGdpdHMgUHR5IEx0ZDAeFw0yMjEwMDUyMjM5MDVaFw0zMjEw MDIyMjM5MDVaMEUxCzAJBgNVBAYTAkFVMRMwEQYDVQQIDApTb21lLVN0YXRlMSEw HwYDVQQKDBhJbnRlcm5ldCBXaWRnaXRzIFB0eSBMdGQwggEiMA0GCSqGSIb3DQEB AQUAA4IBDwAwggEKAoIBAQDMbZctKXBx8v63TXIhM/OB7S6VfPqpzfHufhs6kAHu jfC2ooCUqzqdg0T8bM1bjahYuAbQA1cWKYBsqfd01Po1ltWmbMf7ZvmSB6VN7kC2 Y670zee91dGDQ2yzmorJuIZAtOBVZesYLg8UHSGzSC/smJOrjYidtlbvzOcX0pv3 RCIUrNMed60EpSch/rzAJLzJmwNSQZ4vJHNlNetSkvTi7cxMWfwpcM/rN1hEmP1X J43hJp/TNRZVnEsvs/yggP/FwUjG74mU3KfnWiv91AkkarNTNquEMJ+f4OFqMcnF p0wqg47JTqcAAT0n1B0VB+z0hGXEFMN+IJXsHETZNG+JAgMBAAGjUzBRMB0GA1Ud DgQWBBSIw+qUKQJjXWti5x/Cnn2GueuX5zAfBgNVHSMEGDAWgBSIw+qUKQJjXWti 5x/Cnn2GueuX5zAPBgNVHRMBAf8EBTADAQH/MA0GCSqGSIb3DQEBCwUAA4IBAQAA DXzf8VXi4s2GScNfHf0BzMjpyrtRZ0Wbp2Vfh7OwVR6xcx+pqXNjlydM/vu2LvOK hh7Jbo+JS+o7O24UJ9lLFkCRsZVF+NFqJf+2rdHCaOiZSdZmtjBU0dFuAGS7+lU3 M8P7WCNOm6NAKbs7VZHVcZPzp81SCPQgQIS19xRf4Irbvsijv4YdyL4Qv7aWcclb MdZX9AH9Fx8tJq4VKvUYsCXAD0kuywMLjh+yj5O/2hMvs5rvaQvm2daQNRDNp884 uTLrNF7W7QaKEL06ZpXJoBqdKsiwn577XTDKvzN0XxQrT+xV9VHO7OXblF+Od3/Y SzBR+QiQKy3x+LkOxhkk -----END CERTIFICATE-----` certKey := `-----BEGIN PRIVATE KEY----- MIIEvQIBADANBgkqhkiG9w0BAQEFAASCBKcwggSjAgEAAoIBAQDMbZctKXBx8v63 TXIhM/OB7S6VfPqpzfHufhs6kAHujfC2ooCUqzqdg0T8bM1bjahYuAbQA1cWKYBs qfd01Po1ltWmbMf7ZvmSB6VN7kC2Y670zee91dGDQ2yzmorJuIZAtOBVZesYLg8U HSGzSC/smJOrjYidtlbvzOcX0pv3RCIUrNMed60EpSch/rzAJLzJmwNSQZ4vJHNl NetSkvTi7cxMWfwpcM/rN1hEmP1XJ43hJp/TNRZVnEsvs/yggP/FwUjG74mU3Kfn Wiv91AkkarNTNquEMJ+f4OFqMcnFp0wqg47JTqcAAT0n1B0VB+z0hGXEFMN+IJXs HETZNG+JAgMBAAECggEAM+U6NHfJmNPD/8qER5OFpJ0Ob1qL06F5Yj7XMLWwF9wm mGaGV7dkKOpTD/Wa6Dv82ZDWAeZnLDQa6vr228zZO9Nvp1EEL3kDsCOKvk7WVLbX ikPfKZznE/iA1tNLmkvioPiJ3oQB+2Bt6YA/tuCDcf+FtU43uTm5tiSBIdYQS+Om xN9OEXihk1svxHXQKa/a3nKPVLvdp3P90hDJ0PcRslXSy1V8az+A94JFEnCvnKsK nF2rItCcXkInL0lYHZKgLHQMXGWkNl8e3PA1GZk3yF6LPNtPI1T5Ek9GwkHNw4JZ BL/xEWLKB1qR2Z4I3UbWGVyi418kANv1eISb+49egQKBgQDraSRWB8nM5O3Zl9kT 8S5K924o1oXrO17eqQlVtQVmtUdoVvIBc6uHQZOmV1eHYpr6c95h8apNLexI22AY SWkq9smpCnxLUsdkplwzie0F4bAzD6MCR8WIJxapUSPlyCA+8st1hquYBchKGQhd 6mMY1gzMDacYV/WhtG4E5d0nMQKBgQDeTr793n00VtpKuquFJe6Stu7Ujf64dL0s 3opLovyI0TmtMz5oCqIezwrjqc0Vy0UksWXaz0AboinDP+5n60cTEIt/6H0kryDc dxfSHEA9BBDoQtxOFi3QGcxXbwu0i9QSoexrKY7FhA2xPji6bCcPycthhIrCpUiZ s5gVkjHn2QKBgQCGklxLMbiSgGvXb46Qb9be1AMNJVT427+n2UmUzR6BUC+53boK Sm1LrJkTBerrYdrmQUZnBxcrd40TORT9zTlpbhppn6zeAjwptVAPxlDQg+uNxOqS ayToaC/0KoYy3OxSD8lvLcT56pRMh3LY/RwZHoPCQiu7Js0r21DpS93YgQKBgAuc c09RMprsOmSS0WiX7ZkOIvVJIVfDCSpxySlgLu56dxe7yHOosoUHbVsswEB2KHtd JKPEFWYcFzBSg4I8AK9XOuIIY5jp6L57Hexke1p0fumSrG0LrYLkBg8/Bo58iywZ 9v414nYgipKKXG4oPfYOJShHwvOdrGgSwEvIIgEpAoGAZz0yC9+x+JaoTnyUIRyI +Aj5a4KhYjFtsZhcn/yCZHDqzJNDz6gAu579ey+J2CVOhjtgB5lowsDrHu32Hqnn SEfyTru/ynQ8obwaRzdDYml+On86YWOw+brpMXkN+KB6bs2okE2N68v0qGPakxjt OLDW6kKz5pI4T8lQJhdqjCU= -----END PRIVATE KEY-----` return cert, certKey } func generateCerts2() (string, string) { cert := `-----BEGIN CERTIFICATE----- MIIDazCCAlOgAwIBAgIUSun8oI56ArKxfhqNLLfEmteRHRUwDQYJKoZIhvcNAQEL BQAwRTELMAkGA1UEBhMCQVUxEzARBgNVBAgMClNvbWUtU3RhdGUxITAfBgNVBAoM GEludGVybmV0IFdpZGdpdHMgUHR5IEx0ZDAeFw0yMjEwMDYyMTE1NDdaFw0zMjEw MDMyMTE1NDdaMEUxCzAJBgNVBAYTAkFVMRMwEQYDVQQIDApTb21lLVN0YXRlMSEw HwYDVQQKDBhJbnRlcm5ldCBXaWRnaXRzIFB0eSBMdGQwggEiMA0GCSqGSIb3DQEB AQUAA4IBDwAwggEKAoIBAQC63U6N03rm4I8yFmYK27DUlVdMUGSRQt6UIdPT5F2c fv5mBRLwEANoqscNenNajGHiIqBiFQ3pG+p7BIIq11d87Of24XSll4MoK+6R9SFF 6lTdGt9HSzuCXQtMf5g6/MbgH240xrBXmwwJNkqpUzXVOeQBPzxplf1b/0ircf8n fE81wnCtWyiu8BtlWvs/yJBTvSiIQ6w2Tp+K5oFZLCUwgQZdUcqzXp5nbWZkdO+D hOGdiY7G+fC19GX7lVt+kw+xB/uAqmXw2WoR/Db/M8tJDzTw810ZbWp0tAw7Pga+ ybvIYN9mTFr4Tm052r2jVXAYejf8z4kdr4mCDKlSQTIlAgMBAAGjUzBRMB0GA1Ud DgQWBBRWchX65rXlT+/xlgxhKMTX5/FdtTAfBgNVHSMEGDAWgBRWchX65rXlT+/x lgxhKMTX5/FdtTAPBgNVHRMBAf8EBTADAQH/MA0GCSqGSIb3DQEBCwUAA4IBAQCo I3MOkAULOaa4Vr80lVn/kZi8HIwQ1NenqyoykqO/FDS7q5o5vaeNquqOgC4scTdb WJEgzBNpbIOxEM6ou5Q7IUlX6YZaTMK/Z0QbqjZuHA5ny8uaUERDLoDit318yNe+ 0TOY5m5n+pRkFPvjnqoNNxvYabUqQ7NpgKTv277eecfGdFPi971EiT9HSUM8n7tU 1C1FNr7P1WGmng2EO1UCG3SQi1JpMGUYyFLSOP6F7wWhflO1JqdF57nmTtv8lKJ9 O4ACJ5BuWUqUyDLYjMK+oHh/c6xLHxfQKs62HuLqfaobqUPyE0kS7LXN2G7adjrs 2vBHv2U/QrjmLLF8CSdh -----END CERTIFICATE-----` certKey := `-----BEGIN PRIVATE KEY----- MIIEvQIBADANBgkqhkiG9w0BAQEFAASCBKcwggSjAgEAAoIBAQC63U6N03rm4I8y FmYK27DUlVdMUGSRQt6UIdPT5F2cfv5mBRLwEANoqscNenNajGHiIqBiFQ3pG+p7 BIIq11d87Of24XSll4MoK+6R9SFF6lTdGt9HSzuCXQtMf5g6/MbgH240xrBXmwwJ NkqpUzXVOeQBPzxplf1b/0ircf8nfE81wnCtWyiu8BtlWvs/yJBTvSiIQ6w2Tp+K 5oFZLCUwgQZdUcqzXp5nbWZkdO+DhOGdiY7G+fC19GX7lVt+kw+xB/uAqmXw2WoR /Db/M8tJDzTw810ZbWp0tAw7Pga+ybvIYN9mTFr4Tm052r2jVXAYejf8z4kdr4mC DKlSQTIlAgMBAAECggEANaE4X1n3pvWCA3UMOkeM+6YU1PEpu8r+SHNg8SpUd4q3 Bp6kLcPaxppk4IhpPO6XVShs8VlrkaCSblX/6b29/Tuc420XZkMSwF/Da553uzIi wwZoWHTOEn8TtBPWo+9SQJaksX7os2vrS2WKjgg0pgqkVntIomEKwvGEcLgZ68Gy aCYgrJfvzS38+XhOJB00YOoq6vgqHj8YnTGtYAwwW+nI7oHGJS7H09eQV51cmQ2j NSmc0SsGJ/IYrCMfJp0W8Ho9z66qRiFLb7vFS1050r5r3+slHCZPQwYXY6ovo2EJ 2Y5mKdem70dP8JZx6siVlOCKh/2fHOFNnegcQ/ADgQKBgQDx1ueRb7w9a/lh0PPN 8tLvclN/BJCqVoaF31f+Ah9Q7bfagkI7kmaQfYChWPLM5mXwr8YCPM1jysQOUTJp ExBkGbngv/M0JeXSyt2Z9kbreFSll+ILnImAME+0KKjHTy1gDSvqX/a4NiZdDOaK 44r4CZSeVrpH2YY4tq/huL68xQKBgQDFzlhPEYOxTnQytPuXWRTtB5is1WNs7cU0 AKVGkqgNKj5++Jl+IT3/pDhcJXe06E1V9ldHFpwAorkbIvAEE45aqzp5ZrrlrAjJ 06wmEEgP5tQxmBj+hx6jitzDoEmqHvyN5Dm8/Kxu2VF2n4yTGEeSX+ep1ojLCeAj heJuuO614QKBgGV+O1DeA7IDTnWuq6MS9VNoN4Jm+A+EoJAuW09OtLXSDga2A/Xc Sw74nLMaEUvMpZuNKRxnSAtJXV5k1TMjvQ1FfqzD4d1QylLcsIOcx8aqiVu1kjgt ScdyfwCsz6hVokVdQcDq5TAKCa+jal1/gSL3YlfRLfxZXesPQGEKl4HBAoGBALOw BMye7nDNAgVmHv6Xr8i6k9i9Z7p2LCRXScxYQUzkSS1yi4zmibmG5qPebWXreQVT 6Gjtgv2Y1GpwTHSHh1OaJF5QEgu9QaaGIOXa+Htphu0ea+YbvJt385/KJeDikS4c Ws7xAXsY80W9HigpcCrp8Dp6Zn17FR9v6ggG+uJBAoGAFGo7X1bpEA1bKAA04wJL gq6wwKgTUjqnvHSo1CqPqoWeX8MM0VU9Jw2n0bxfD5He/snYO4pQUatD90kcgQch BmvE1yTn4kzC0ZO3++qPulpXpAp4QJLIdKeAE9cPhKqe4lBboJRbJqoXCaoIxNeg z0xcfR+tEmGlvxaHqXlQg9o= -----END PRIVATE KEY-----` return cert, certKey } ================================================ FILE: ddosify_engine/core/proxy/base.go ================================================ /* * * Ddosify - Load testing tool for any web system. * Copyright (C) 2021 Ddosify (https://ddosify.com) * * This program is free software: you can redistribute it and/or modify * it under the terms of the GNU Affero General Public License as published * by the Free Software Foundation, either version 3 of the License, or * (at your option) any later version. * * This program is distributed in the hope that it will be useful, * but WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * GNU Affero General Public License for more details. * * You should have received a copy of the GNU Affero General Public License * along with this program. If not, see . * */ package proxy import ( "fmt" "net/url" "reflect" ) var AvailableProxyServices = make(map[string]ProxyService) // Proxy struct is used for initializing the ProxyService implementations. type Proxy struct { // Stragy of the proxy usage. Strategy string // Set this field if ProxyStrategy is single Addr *url.URL // Dynamic field for other proxy strategies. Others map[string]interface{} } // ProvideService is the interface that abstracts different proxy implementations. // Strategy field in types.Proxy determines which implementation to use. type ProxyService interface { Init(Proxy) error GetAll() []*url.URL GetProxy() *url.URL ReportProxy(addr *url.URL, reason string) *url.URL GetProxyCountry(*url.URL) string Done() error } // NewProxyService is the factory method of the ProxyService. func NewProxyService(s string) (service ProxyService, err error) { if val, ok := AvailableProxyServices[s]; ok { // Create a new object from the service type service = reflect.New(reflect.TypeOf(val).Elem()).Interface().(ProxyService) } else { err = fmt.Errorf("unsupported proxy strategy: %s", s) } return } ================================================ FILE: ddosify_engine/core/proxy/base_test.go ================================================ /* * * Ddosify - Load testing tool for any web system. * Copyright (C) 2021 Ddosify (https://ddosify.com) * * This program is free software: you can redistribute it and/or modify * it under the terms of the GNU Affero General Public License as published * by the Free Software Foundation, either version 3 of the License, or * (at your option) any later version. * * This program is distributed in the hope that it will be useful, * but WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * GNU Affero General Public License for more details. * * You should have received a copy of the GNU Affero General Public License * along with this program. If not, see . * */ package proxy import ( "testing" ) func TestNewProxyService(t *testing.T) { // Valid proxy types for k := range AvailableProxyServices { _, err := NewProxyService(k) if err != nil { t.Errorf("TestNewProxyService errored %v", err) } } // Invalid proxy type _, err := NewProxyService("invalid_proxy_type") if err == nil { t.Errorf("TestNewProxyService invalid proxy should errored") } } ================================================ FILE: ddosify_engine/core/proxy/single.go ================================================ /* * * Ddosify - Load testing tool for any web system. * Copyright (C) 2021 Ddosify (https://ddosify.com) * * This program is free software: you can redistribute it and/or modify * it under the terms of the GNU Affero General Public License as published * by the Free Software Foundation, either version 3 of the License, or * (at your option) any later version. * * This program is distributed in the hope that it will be useful, * but WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * GNU Affero General Public License for more details. * * You should have received a copy of the GNU Affero General Public License * along with this program. If not, see . * */ package proxy import ( "net/url" ) const ProxyTypeSingle = "single" func init() { AvailableProxyServices[ProxyTypeSingle] = &singleProxyStrategy{} } type singleProxyStrategy struct { proxyAddr *url.URL } func (sp *singleProxyStrategy) Init(p Proxy) error { sp.proxyAddr = p.Addr return nil } // Since there is a 1 proxy, return that always func (sp *singleProxyStrategy) GetAll() []*url.URL { return []*url.URL{sp.proxyAddr} } // Since there is a 1 proxy, return that always func (sp *singleProxyStrategy) GetProxy() *url.URL { return sp.proxyAddr } func (sp *singleProxyStrategy) ReportProxy(addr *url.URL, reason string) *url.URL { return sp.proxyAddr } func (sp *singleProxyStrategy) GetProxyCountry(addr *url.URL) string { return "unknown" } func (sp *singleProxyStrategy) Done() error { return nil } ================================================ FILE: ddosify_engine/core/report/aggregator.go ================================================ /* * * Ddosify - Load testing tool for any web system. * Copyright (C) 2021 Ddosify (https://ddosify.com) * * This program is free software: you can redistribute it and/or modify * it under the terms of the GNU Affero General Public License as published * by the Free Software Foundation, either version 3 of the License, or * (at your option) any later version. * * This program is distributed in the hope that it will be useful, * but WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * GNU Affero General Public License for more details. * * You should have received a copy of the GNU Affero General Public License * along with this program. If not, see . * */ package report import ( "strings" "time" "go.ddosify.com/ddosify/core/assertion" "go.ddosify.com/ddosify/core/types" ) func aggregate(result *Result, scr *types.ScenarioResult, samplingCount map[uint16]map[string]int, samplingRate int) { var scenarioDuration float32 errOccured := false assertionFail := false for _, sr := range scr.StepResults { scenarioDuration += float32(sr.Duration.Seconds()) fv := FailVerbose{} fv.AssertionErrorDist.Conditions = make(map[string]*AssertInfo) fv.ServerErrorDist.Reasons = make(map[string]int) if _, ok := result.StepResults[sr.StepID]; !ok { result.StepResults[sr.StepID] = &ScenarioStepResultSummary{ Name: sr.StepName, StatusCodeDist: make(map[int]int, 0), Fail: fv, Durations: map[string]float32{}, SuccessCount: 0, } } stepResult := result.StepResults[sr.StepID] if len(sr.FailedAssertions) > 0 { // assertion error errOccured = true assertionFail = true stepResult.Fail.Count++ stepResult.Fail.AssertionErrorDist.Count++ stepResult.StatusCodeDist[sr.StatusCode]++ for _, fa := range sr.FailedAssertions { if aed, ok := stepResult.Fail.AssertionErrorDist.Conditions[fa.Rule]; !ok { samplingCount[sr.StepID] = make(map[string]int) samplingCount[sr.StepID][fa.Rule] = 1 ae := &AssertInfo{ Count: 1, Received: make(map[string][]interface{}), Reason: fa.Reason, } for ident, value := range fa.Received { ae.Received[ident] = []interface{}{value} } stepResult.Fail.AssertionErrorDist.Conditions[fa.Rule] = ae } else { aed.Count++ samplingCount[sr.StepID][fa.Rule]++ if samplingCount[sr.StepID][fa.Rule] <= samplingRate { for ident, value := range fa.Received { // do not append if the value is already in the list exists := false for _, v := range aed.Received[ident] { if v == value { exists = true } } if !exists { aed.Received[ident] = append(aed.Received[ident], value) } } } } } totalDur := float32(stepResult.SuccessCount+stepResult.Fail.Count-1)*stepResult.Durations["duration"] + float32(sr.Duration.Seconds()) stepResult.Durations["duration"] = totalDur / float32(stepResult.SuccessCount+stepResult.Fail.Count) for k, v := range sr.Custom { if strings.Contains(k, "Duration") { totalDur := float32(stepResult.SuccessCount+stepResult.Fail.Count-1)*stepResult.Durations[k] + float32(v.(time.Duration).Seconds()) stepResult.Durations[k] = float32(totalDur / float32(stepResult.SuccessCount+stepResult.Fail.Count)) } } } else if sr.Err.Type != "" { // server error errOccured = true stepResult.Fail.Count++ stepResult.Fail.ServerErrorDist.Count++ stepResult.Fail.ServerErrorDist.Reasons[sr.Err.Reason]++ } else { // success stepResult.StatusCodeDist[sr.StatusCode]++ stepResult.SuccessCount++ totalDur := float32(stepResult.SuccessCount+stepResult.Fail.Count-1)*stepResult.Durations["duration"] + float32(sr.Duration.Seconds()) stepResult.Durations["duration"] = totalDur / float32(stepResult.SuccessCount+stepResult.Fail.Count) for k, v := range sr.Custom { if strings.Contains(k, "Duration") { totalDur := float32(stepResult.SuccessCount-1)*stepResult.Durations[k] + float32(v.(time.Duration).Seconds()) stepResult.Durations[k] = float32(totalDur / float32(stepResult.SuccessCount+stepResult.Fail.Count)) } } } } // Don't change avg duration if there is a error if !errOccured { totalDuration := float32(result.SuccessCount)*result.AvgDuration + scenarioDuration result.SuccessCount++ result.AvgDuration = totalDuration / float32(result.SuccessCount) } else if errOccured { if assertionFail { // if any step failed because of assertion, that iteration counts as assertion fail result.AssertionFailCount++ } else { // server error result.ServerFailedCount++ } } } // Total test result, all scenario iterations combined type Result struct { TestStatus string `json:"test_status"` TestFailedAssertions []assertion.FailedRule `json:"failed_criterias"` SuccessCount int64 `json:"success_count"` ServerFailedCount int64 `json:"server_fail_count"` AssertionFailCount int64 `json:"assertion_fail_count"` AvgDuration float32 `json:"avg_duration"` StepResults map[uint16]*ScenarioStepResultSummary `json:"steps"` } func (r *Result) successPercentage() int { if r.SuccessCount+r.ServerFailedCount+r.AssertionFailCount == 0 { return 0 } t := float32(r.SuccessCount) / float32(r.SuccessCount+r.ServerFailedCount+r.AssertionFailCount) return int(t * 100) } func (r *Result) failedPercentage() int { if r.SuccessCount+r.ServerFailedCount+r.AssertionFailCount == 0 { return 0 } return 100 - r.successPercentage() } type AssertionErrVerbose struct { Count int64 `json:"count"` Conditions map[string]*AssertInfo `json:"conditions"` } type ServerErrVerbose struct { Count int64 `json:"count"` Reasons map[string]int `json:"reasons"` } type FailVerbose struct { Count int64 `json:"count"` AssertionErrorDist AssertionErrVerbose `json:"assertions"` ServerErrorDist ServerErrVerbose `json:"server"` } type ScenarioStepResultSummary struct { Name string `json:"name"` StatusCodeDist map[int]int `json:"status_code_dist"` Fail FailVerbose `json:"fail"` Durations map[string]float32 `json:"durations"` SuccessCount int64 `json:"success_count"` } func (s *ScenarioStepResultSummary) successPercentage() int { if s.SuccessCount+s.Fail.Count == 0 { return 0 } t := float32(s.SuccessCount) / float32(s.SuccessCount+s.Fail.Count) return int(t * 100) } func (s *ScenarioStepResultSummary) failedPercentage() int { if s.SuccessCount+s.Fail.Count == 0 { return 0 } return 100 - s.successPercentage() } type AssertInfo struct { Count int Received map[string][]interface{} Reason string } ================================================ FILE: ddosify_engine/core/report/aggregator_test.go ================================================ package report import ( "reflect" "testing" "time" "go.ddosify.com/ddosify/core/types" ) func TestStart(t *testing.T) { responses := []*types.ScenarioResult{ { StartTime: time.Now(), StepResults: []*types.ScenarioStepResult{ { StepID: 1, StatusCode: 200, RequestTime: time.Now().Add(1), Duration: time.Duration(10) * time.Second, Custom: map[string]interface{}{ "dnsDuration": time.Duration(5) * time.Second, "connDuration": time.Duration(5) * time.Second, }, }, { StepID: 2, RequestTime: time.Now().Add(2), Duration: time.Duration(30) * time.Second, Err: types.RequestError{Type: types.ErrorConn, Reason: types.ReasonConnTimeout}, Custom: map[string]interface{}{ "dnsDuration": time.Duration(10) * time.Second, "connDuration": time.Duration(20) * time.Second, }, }, { StepID: 3, StatusCode: 400, RequestTime: time.Now().Add(2), Duration: time.Duration(30) * time.Second, Custom: map[string]interface{}{ "dnsDuration": time.Duration(10) * time.Second, "connDuration": time.Duration(20) * time.Second, }, FailedAssertions: []types.FailedAssertion{{ Rule: "equals(status_code,200)", Received: map[string]interface{}{"status_code": 400}, }}, }, }, }, { StartTime: time.Now().Add(10), StepResults: []*types.ScenarioStepResult{ { StepID: 1, StatusCode: 200, RequestTime: time.Now().Add(11), Duration: time.Duration(30) * time.Second, Custom: map[string]interface{}{ "dnsDuration": time.Duration(10) * time.Second, "connDuration": time.Duration(20) * time.Second, }, }, { StepID: 2, StatusCode: 401, RequestTime: time.Now().Add(12), Duration: time.Duration(60) * time.Second, Custom: map[string]interface{}{ "dnsDuration": time.Duration(20) * time.Second, "connDuration": time.Duration(40) * time.Second, }, }, { StepID: 3, StatusCode: 200, RequestTime: time.Now().Add(2), Duration: time.Duration(30) * time.Second, Custom: map[string]interface{}{ "dnsDuration": time.Duration(10) * time.Second, "connDuration": time.Duration(20) * time.Second, }, }, }, }, } fail1 := FailVerbose{} fail1.Count = 0 fail1.ServerErrorDist.Count = 0 fail1.ServerErrorDist.Reasons = make(map[string]int) fail1.AssertionErrorDist.Conditions = make(map[string]*AssertInfo) itemReport1 := &ScenarioStepResultSummary{ StatusCodeDist: map[int]int{200: 2}, SuccessCount: 2, Fail: fail1, Durations: map[string]float32{ "dnsDuration": 7.5, "connDuration": 12.5, "duration": 20, }, } fail2 := FailVerbose{} fail2.Count = 1 fail2.ServerErrorDist.Count = 1 fail2.ServerErrorDist.Reasons = make(map[string]int) fail2.ServerErrorDist.Reasons[types.ReasonConnTimeout] = 1 fail2.AssertionErrorDist.Conditions = make(map[string]*AssertInfo) itemReport2 := &ScenarioStepResultSummary{ StatusCodeDist: map[int]int{401: 1}, SuccessCount: 1, Fail: fail2, Durations: map[string]float32{ "dnsDuration": 20, "connDuration": 40, "duration": 60, }, } fail3 := FailVerbose{} fail3.Count = 1 fail3.AssertionErrorDist.Count = 1 fail3.ServerErrorDist.Reasons = make(map[string]int) fail3.AssertionErrorDist.Conditions = make(map[string]*AssertInfo) fail3.AssertionErrorDist.Conditions["equals(status_code,200)"] = &AssertInfo{ Count: 1, Received: map[string][]interface{}{ "status_code": {400}, }, } itemReport3 := &ScenarioStepResultSummary{ StatusCodeDist: map[int]int{400: 1, 200: 1}, SuccessCount: 1, Fail: fail3, Durations: map[string]float32{ "dnsDuration": 20, "connDuration": 40, "duration": 60, }, } expectedResult := Result{ SuccessCount: 1, ServerFailedCount: 0, AssertionFailCount: 1, AvgDuration: 120, StepResults: map[uint16]*ScenarioStepResultSummary{ uint16(1): itemReport1, uint16(2): itemReport2, uint16(3): itemReport3, }, } s := &stdout{} debug := false s.Init(debug, 0) responseChan := make(chan *types.ScenarioResult, len(responses)) go s.Start(responseChan, nil) go func() { for _, r := range responses { responseChan <- r } close(responseChan) }() doneChanSignaled := false select { case <-s.doneChan: doneChanSignaled = true case <-time.After(time.Duration(1) * time.Second): } if !doneChanSignaled { t.Errorf("DoneChan is not signaled") } if !compareResults(s.result, &expectedResult) { t.Errorf("Expected %#v, Found %#v", s.result, expectedResult) } } func compareResults(r1, r2 *Result) bool { if r1.successPercentage() != r2.successPercentage() || r1.failedPercentage() != r2.failedPercentage() || r1.SuccessCount != r2.SuccessCount || r1.AvgDuration != r2.AvgDuration || r1.ServerFailedCount != r2.ServerFailedCount || r1.AssertionFailCount != r2.AssertionFailCount { return false } for stepId, sr := range r1.StepResults { if !compareStepResults(sr, r2.StepResults[stepId]) { return false } } return true } func compareStepResults(s1, s2 *ScenarioStepResultSummary) bool { if s1.successPercentage() != s2.successPercentage() || s1.failedPercentage() != s2.failedPercentage() || s1.SuccessCount != s2.SuccessCount || s1.Name != s2.Name || s1.Fail.Count != s2.Fail.Count || s1.Fail.AssertionErrorDist.Count != s2.Fail.AssertionErrorDist.Count || s1.Fail.ServerErrorDist.Count != s2.Fail.ServerErrorDist.Count || !reflect.DeepEqual(s1.Fail.AssertionErrorDist.Conditions, s2.Fail.AssertionErrorDist.Conditions) || !reflect.DeepEqual(s1.Fail.ServerErrorDist.Reasons, s2.Fail.ServerErrorDist.Reasons) { return false } return true } ================================================ FILE: ddosify_engine/core/report/base.go ================================================ /* * * Ddosify - Load testing tool for any web system. * Copyright (C) 2021 Ddosify (https://ddosify.com) * * This program is free software: you can redistribute it and/or modify * it under the terms of the GNU Affero General Public License as published * by the Free Software Foundation, either version 3 of the License, or * (at your option) any later version. * * This program is distributed in the hope that it will be useful, * but WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * GNU Affero General Public License for more details. * * You should have received a copy of the GNU Affero General Public License * along with this program. If not, see . * */ package report import ( "fmt" "reflect" "go.ddosify.com/ddosify/core/assertion" "go.ddosify.com/ddosify/core/types" ) var AvailableOutputServices = make(map[string]ReportService) // ReportService is the interface that abstracts different report implementations. type ReportService interface { DoneChan() <-chan bool Init(debug bool, samplingRate int) error Start(input chan *types.ScenarioResult, assertionResultChan <-chan assertion.TestAssertionResult) } // NewReportService is the factory method of the ReportService. func NewReportService(s string) (service ReportService, err error) { if val, ok := AvailableOutputServices[s]; ok { // Create a new object from the service type service = reflect.New(reflect.TypeOf(val).Elem()).Interface().(ReportService) } else { err = fmt.Errorf("unsupported output type: %s", s) } return } ================================================ FILE: ddosify_engine/core/report/base_test.go ================================================ /* * * Ddosify - Load testing tool for any web system. * Copyright (C) 2021 Ddosify (https://ddosify.com) * * This program is free software: you can redistribute it and/or modify * it under the terms of the GNU Affero General Public License as published * by the Free Software Foundation, either version 3 of the License, or * (at your option) any later version. * * This program is distributed in the hope that it will be useful, * but WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * GNU Affero General Public License for more details. * * You should have received a copy of the GNU Affero General Public License * along with this program. If not, see . * */ package report import ( "testing" ) func TestNewReportService(t *testing.T) { // Valid output types for k := range AvailableOutputServices { _, err := NewReportService(k) if err != nil { t.Errorf("TestNewReportService %v", err) } } // Invalid output type _, err := NewReportService("invalid_output_type") if err == nil { t.Errorf("TestNewReportService invalid output should errored") } } ================================================ FILE: ddosify_engine/core/report/debug.go ================================================ package report import ( "encoding/json" "html" "net/http" "strings" "go.ddosify.com/ddosify/core/types" ) type verboseRequest struct { Url string `json:"url"` Method string `json:"method"` Headers map[string]string `json:"headers"` Body interface{} `json:"body"` } type verboseResponse struct { StatusCode int `json:"status_code"` Headers map[string]string `json:"headers"` Body interface{} `json:"body"` ResponseTime int64 `json:"response_time"` // in milliseconds } type verboseHttpRequestInfo struct { StepId uint16 `json:"step_id"` StepName string `json:"step_name"` Request verboseRequest `json:"request"` Response verboseResponse `json:"response"` Envs map[string]interface{} `json:"envs"` TestData map[string]interface{} `json:"test_data"` FailedCaptures map[string]string `json:"failed_captures"` FailedAssertions []types.FailedAssertion `json:"failed_assertions"` Error string `json:"error"` } func ScenarioStepResultToVerboseHttpRequestInfo(sr *types.ScenarioStepResult) verboseHttpRequestInfo { var verboseInfo verboseHttpRequestInfo verboseInfo.StepId = sr.StepID verboseInfo.StepName = sr.StepName if sr.Err.Type == types.ErrorInvalidRequest { // could not prepare request at all verboseInfo.Error = sr.Err.Error() return verboseInfo } requestHeaders, requestBody, _ := decode(sr.ReqHeaders, sr.ReqBody) verboseInfo.Request = struct { Url string "json:\"url\"" Method string "json:\"method\"" Headers map[string]string "json:\"headers\"" Body interface{} "json:\"body\"" }{ Url: sr.Url, Method: sr.Method, Headers: requestHeaders, Body: requestBody, } if sr.Err.Type != "" { verboseInfo.Error = sr.Err.Error() } else { responseHeaders, responseBody, _ := decode(sr.RespHeaders, sr.RespBody) // TODO what to do with error verboseInfo.Response = verboseResponse{ StatusCode: sr.StatusCode, Headers: responseHeaders, Body: responseBody, ResponseTime: sr.Duration.Milliseconds(), } } envs := make(map[string]interface{}) testData := make(map[string]interface{}) for key, val := range sr.UsableEnvs { if strings.HasPrefix(key, "data.") { testData[key] = val } else { envs[key] = val } } verboseInfo.Envs = envs verboseInfo.TestData = testData verboseInfo.FailedCaptures = sr.FailedCaptures verboseInfo.FailedAssertions = sr.FailedAssertions return verboseInfo } func decode(headers http.Header, byteBody []byte) (map[string]string, interface{}, error) { contentType := headers.Get("Content-Type") var reqBody interface{} hs := make(map[string]string, 0) for k, v := range headers { values := strings.Join(v, ",") hs[k] = values } if strings.Contains(contentType, "text/html") { unescapedHmtl := html.UnescapeString(string(byteBody)) reqBody = unescapedHmtl } else if strings.Contains(contentType, "application/json") { err := json.Unmarshal(byteBody, &reqBody) if err != nil { reqBody = string(byteBody) } } else { // for remaining content-types return plain string // xml.Unmarshal() needs xml tags to decode encoded xml, we have no knowledge about the xml structure reqBody = string(byteBody) } return hs, reqBody, nil } func isVerboseInfoRequestEmpty(req verboseRequest) bool { if req.Url == "" && req.Method == "" && req.Headers == nil && req.Body == nil { return true } return false } ================================================ FILE: ddosify_engine/core/report/debug_test.go ================================================ package report import ( "encoding/json" "net/http" "reflect" "testing" ) func TestDecode(t *testing.T) { h := http.Header{} h.Add("Content-Type", "application/json") type Temp struct { X float64 `json:"x"` } body := Temp{ X: 52.2, } bBody, _ := json.Marshal(body) _, bodyDecoded, err := decode(h, bBody) if err != nil { t.Errorf("%v", err) } expected := reflect.ValueOf(map[string]interface{}{"x": 52.2}) ei := expected.Interface() if !reflect.DeepEqual(ei, bodyDecoded) { t.Errorf("TestDecode, expected:%s got:%s", ei, bodyDecoded) } } ================================================ FILE: ddosify_engine/core/report/stdout.go ================================================ /* * * Ddosify - Load testing tool for any web system. * Copyright (C) 2021 Ddosify (https://ddosify.com) * * This program is free software: you can redistribute it and/or modify * it under the terms of the GNU Affero General Public License as published * by the Free Software Foundation, either version 3 of the License, or * (at your option) any later version. * * This program is distributed in the hope that it will be useful, * but WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * GNU Affero General Public License for more details. * * You should have received a copy of the GNU Affero General Public License * along with this program. If not, see . * */ package report import ( "encoding/json" "fmt" "io" "net/http" "sort" "strings" "sync" "text/tabwriter" "time" "github.com/enescakir/emoji" "github.com/fatih/color" "github.com/mattn/go-colorable" "go.ddosify.com/ddosify/core/assertion" "go.ddosify.com/ddosify/core/types" "go.ddosify.com/ddosify/core/util" ) const OutputTypeStdout = "stdout" var out = colorable.NewColorableStdout() func init() { AvailableOutputServices[OutputTypeStdout] = &stdout{} } type stdout struct { doneChan chan bool result *Result printTicker *time.Ticker mu sync.Mutex debug bool samplingRate int } var white = color.New(color.FgHiWhite).SprintFunc() var blue = color.New(color.FgHiBlue).SprintFunc() var green = color.New(color.FgHiGreen).SprintFunc() var yellow = color.New(color.FgHiYellow).SprintFunc() var red = color.New(color.FgHiRed).SprintFunc() var realTimePrintInterval = time.Duration(1500) * time.Millisecond func (s *stdout) Init(debug bool, samplingRate int) (err error) { s.doneChan = make(chan bool, 1) s.result = &Result{ StepResults: make(map[uint16]*ScenarioStepResultSummary), } s.debug = debug s.samplingRate = samplingRate color.Cyan("%s Initializing... \n", emoji.Gear) if s.debug { color.Cyan("%s Running in debug mode, 1 iteration will be played... \n", emoji.Bug) } return } func (s *stdout) Start(input chan *types.ScenarioResult, assertionResultChan <-chan assertion.TestAssertionResult) { if s.debug { s.result.TestStatus = "success" if assertionResultChan != nil { result := <-assertionResultChan if result.Fail { s.result.TestStatus = "failed" s.result.TestFailedAssertions = result.FailedRules } } s.printInDebugMode(input) if s.result.TestStatus == "success" { s.doneChan <- true } else { s.doneChan <- false } return } go s.realTimePrintStart() stopSampling := make(chan struct{}) samplingCount := make(map[uint16]map[string]int) go s.cleanSamplingCount(samplingCount, stopSampling, s.samplingRate) for r := range input { s.mu.Lock() // avoid race around samplingCount aggregate(s.result, r, samplingCount, s.samplingRate) s.mu.Unlock() } // listen for assertion result s.result.TestStatus = "success" if assertionResultChan != nil { result := <-assertionResultChan if result.Fail { s.result.TestStatus = "failed" s.result.TestFailedAssertions = result.FailedRules } } s.realTimePrintStop() s.report() stopSampling <- struct{}{} if s.result.TestStatus == "success" { s.doneChan <- true } else { s.doneChan <- false } } func (s *stdout) cleanSamplingCount(samplingCount map[uint16]map[string]int, stopSampling chan struct{}, samplingRate int) { ticker := time.NewTicker(1 * time.Second) for { select { case <-ticker.C: s.mu.Lock() // avoid race around samplingCount for stepId, ruleMap := range samplingCount { for rule, count := range ruleMap { if count >= samplingRate { samplingCount[stepId][rule] = 0 } } } s.mu.Unlock() case <-stopSampling: return } } } func (s *stdout) report() { s.printDetails() } func (s *stdout) DoneChan() <-chan bool { return s.doneChan } func (s *stdout) realTimePrintStart() { if util.IsSystemInTestMode() { return } s.printTicker = time.NewTicker(realTimePrintInterval) color.Cyan("%s Engine fired. \n\n", emoji.Fire) color.Cyan("%s CTRL+C to gracefully stop.\n", emoji.StopSign) for range s.printTicker.C { go func() { s.mu.Lock() s.liveResultPrint() s.mu.Unlock() }() } } func (s *stdout) liveResultPrint() { fmt.Fprintf(out, "%s %s %s\n", green(fmt.Sprintf("%s Successful Run: %-6d %3d%% %5s", emoji.CheckMark, s.result.SuccessCount, s.result.successPercentage(), "")), red(fmt.Sprintf("%s Failed Run: %-6d %3d%% %5s", emoji.CrossMark, s.result.ServerFailedCount+s.result.AssertionFailCount, s.result.failedPercentage(), "")), blue(fmt.Sprintf("%s Avg. Duration: %.5fs", emoji.Stopwatch, s.result.AvgDuration))) } func (s *stdout) realTimePrintStop() { if util.IsSystemInTestMode() { return } // Last print. s.liveResultPrint() s.printTicker.Stop() } func (s *stdout) printInDebugMode(input chan *types.ScenarioResult) { color.Cyan("%s Engine fired. \n\n", emoji.Fire) color.Cyan("%s CTRL+C to gracefully stop.\n", emoji.StopSign) for r := range input { // only 1 ScenarioResult expected for _, sr := range r.StepResults { verboseInfo := ScenarioStepResultToVerboseHttpRequestInfo(sr) b := strings.Builder{} w := tabwriter.NewWriter(&b, 0, 0, 4, ' ', 0) color.Cyan("\n\nSTEP (%d) %-5s\n", verboseInfo.StepId, verboseInfo.StepName) color.Cyan("-------------------------------------") if len(verboseInfo.Envs) > 0 { fmt.Fprintf(w, "%s\n", blue("- Environment Variables")) for eKey, eVal := range verboseInfo.Envs { switch eVal.(type) { case map[string]interface{}: valPretty, _ := json.Marshal(eVal) fmt.Fprintf(w, "\t%s:\t%-5s \n", fmt.Sprint(eKey), valPretty) case []string: valPretty, _ := json.Marshal(eVal) fmt.Fprintf(w, "\t%s:\t%-5s \n", fmt.Sprint(eKey), valPretty) case []float64: valPretty, _ := json.Marshal(eVal) fmt.Fprintf(w, "\t%s:\t%-5s \n", fmt.Sprint(eKey), valPretty) case []bool: valPretty, _ := json.Marshal(eVal) fmt.Fprintf(w, "\t%s:\t%-5s \n", fmt.Sprint(eKey), valPretty) default: fmt.Fprintf(w, "\t%s:\t%-5s \n", fmt.Sprint(eKey), fmt.Sprint(eVal)) } } fmt.Fprintf(w, "\n") } if len(verboseInfo.TestData) > 0 { fmt.Fprintf(w, "%s\n", blue("- Test Data")) for eKey, eVal := range verboseInfo.TestData { switch eVal.(type) { case map[string]interface{}: valPretty, _ := json.Marshal(eVal) fmt.Fprintf(w, "\t%s:\t%-5s \n", fmt.Sprint(eKey), valPretty) case []int: valPretty, _ := json.Marshal(eVal) fmt.Fprintf(w, "\t%s:\t%-5s \n", fmt.Sprint(eKey), valPretty) case []string: valPretty, _ := json.Marshal(eVal) fmt.Fprintf(w, "\t%s:\t%-5s \n", fmt.Sprint(eKey), valPretty) case []float64: valPretty, _ := json.Marshal(eVal) fmt.Fprintf(w, "\t%s:\t%-5s \n", fmt.Sprint(eKey), valPretty) case []bool: valPretty, _ := json.Marshal(eVal) fmt.Fprintf(w, "\t%s:\t%-5s \n", fmt.Sprint(eKey), valPretty) default: fmt.Fprintf(w, "\t%s:\t%-5s \n", fmt.Sprint(eKey), fmt.Sprint(eVal)) } } fmt.Fprintf(w, "\n") } if verboseInfo.Error != "" && isVerboseInfoRequestEmpty(verboseInfo.Request) { fmt.Fprintf(w, "%s Error: \t%-5s \n", emoji.SosButton, verboseInfo.Error) fmt.Fprintln(w) fmt.Fprint(out, b.String()) break } fmt.Fprintf(w, "%s\n", blue("- Request")) fmt.Fprintf(w, "\tTarget: \t%s \n", verboseInfo.Request.Url) fmt.Fprintf(w, "\tMethod: \t%s \n", verboseInfo.Request.Method) fmt.Fprintf(w, "\t%s\n", "Headers: ") for hKey, hVal := range verboseInfo.Request.Headers { fmt.Fprintf(w, "\t\t%s:\t%-5s \n", hKey, hVal) } contentType := sr.ReqHeaders.Get("content-type") fmt.Fprintf(w, "\t%s", "Body: ") printBody(w, contentType, verboseInfo.Request.Body) fmt.Fprintf(w, "\n") if verboseInfo.Error == "" { // response fmt.Fprintf(w, "\n%s\n", blue("- Response")) fmt.Fprintf(w, "\tStatusCode:\t%-5d \n", verboseInfo.Response.StatusCode) fmt.Fprintf(w, "\tResponseTime:\t%-5d(ms) \n", verboseInfo.Response.ResponseTime) fmt.Fprintf(w, "\t%s\n", "Headers: ") for hKey, hVal := range verboseInfo.Response.Headers { fmt.Fprintf(w, "\t\t%s:\t%-5s \n", hKey, hVal) } contentType := sr.RespHeaders.Get("content-type") fmt.Fprintf(w, "\t%s", "Body: ") printBody(w, contentType, verboseInfo.Response.Body) fmt.Fprintf(w, "\n") } if len(verboseInfo.FailedCaptures) > 0 { fmt.Fprintf(w, "%s\n", yellow("- Failed Captures")) for wKey, wVal := range verboseInfo.FailedCaptures { fmt.Fprintf(w, "\t\t%s: \t%s \n", wKey, wVal) } } if len(verboseInfo.FailedAssertions) > 0 { fmt.Fprintf(w, "%s", yellow("- Failed Assertions")) for _, failAssertion := range verboseInfo.FailedAssertions { fmt.Fprintf(w, "\n\t\tRule: %s\n", failAssertion.Rule) prettyReceived, _ := json.MarshalIndent(failAssertion.Received, "\t\t", "\t") fmt.Fprintf(w, "\t\tReceived: %s\n", prettyReceived) fmt.Fprintf(w, "\t\tReason: %s\n", failAssertion.Reason) } } if verboseInfo.Error != "" { // server error fmt.Fprintf(w, "\n%s Error: \t%-5s \n", emoji.SosButton, verboseInfo.Error) } fmt.Fprintln(w) fmt.Fprint(out, b.String()) } } b := strings.Builder{} w := tabwriter.NewWriter(&b, 0, 0, 4, ' ', 0) if s.result.TestStatus == "success" { fmt.Fprintf(w, "%s", green("Test Status : Success\n")) } else if s.result.TestStatus == "failed" { fmt.Fprintf(w, "\n%s", red("Test Status: Failed\n")) for _, failedRule := range s.result.TestFailedAssertions { fmt.Fprintf(w, red("\nRule: %s\n"), failedRule.Rule) fmt.Fprintf(w, red("Received: \n")) for ident, values := range failedRule.ReceivedMap { fmt.Fprintf(w, red("\t%s: %v\n"), ident, values) } } } fmt.Fprintln(w) fmt.Fprint(out, b.String()) } func printBody(w io.Writer, contentType string, body interface{}) { if body == nil { return } if strings.Contains(contentType, "application/json") { valPretty, _ := json.MarshalIndent(body, "\t\t", "\t") fmt.Fprintf(w, "\n\t\t%s\n", valPretty) } else { // html unescaped text // if xml came as decoded, we could pretty print it like json fmt.Fprintf(w, "%+v\n", fmt.Sprintf("%s", body)) } } // TODO:REFACTOR use template func (s *stdout) printDetails() { color.Set(color.FgHiCyan) defer color.Unset() b := strings.Builder{} w := tabwriter.NewWriter(&b, 0, 0, 4, ' ', 0) fmt.Fprintln(w, "\n\nRESULT") fmt.Fprintln(w, "-------------------------------------") keys := make([]int, 0) for k := range s.result.StepResults { keys = append(keys, int(k)) } // Since map is not a ordered data structure, // We should sort scenarioItemIDs to traverse itemReports sort.Ints(keys) for _, k := range keys { v := s.result.StepResults[uint16(k)] if len(keys) > 1 { stepHeader := v.Name if v.Name == "" { stepHeader = fmt.Sprintf("Step %d", k) } fmt.Fprintf(w, "\n%d. "+stepHeader+"\n", k) fmt.Fprintln(w, "---------------------------------") } fmt.Fprintf(w, "Success Count:\t%-5d (%d%%)\n", v.SuccessCount, v.successPercentage()) fmt.Fprintf(w, "Failed Count:\t%-5d (%d%%)\n", v.Fail.Count, v.failedPercentage()) fmt.Fprintln(w, "\nDurations (Avg):") var durationList = make([]duration, 0) for d, s := range v.Durations { dur := keyToStr[d] dur.duration = s durationList = append(durationList, dur) } sort.Slice(durationList, func(i, j int) bool { return durationList[i].order < durationList[j].order }) for _, v := range durationList { fmt.Fprintf(w, " %s\t:%.4fs\n", v.name, v.duration) } if len(v.StatusCodeDist) > 0 { fmt.Fprintln(w, "\nStatus Code (Message) :Count") for s, c := range v.StatusCodeDist { desc := fmt.Sprintf("%3d (%s)", s, http.StatusText(s)) fmt.Fprintf(w, " %s\t:%d\n", desc, c) } } if v.Fail.AssertionErrorDist.Count > 0 { fmt.Fprintln(w, "\nAssertion Error Distribution:") for e, c := range v.Fail.AssertionErrorDist.Conditions { fmt.Fprintf(w, "\tCondition: %s\n", e) fmt.Fprintf(w, "\t\tFail Count: %d\n", c.Count) fmt.Fprintf(w, "\t\tReceived: \n") for ident, values := range c.Received { // deduplication values = deduplicate(values) fmt.Fprintf(w, "\t\t\t %s : %v\n", ident, values) } fmt.Fprintf(w, "\t\tReason: %s \n\n", c.Reason) } } if v.Fail.ServerErrorDist.Count > 0 { fmt.Fprintln(w, "\nServer Error Distribution (Count:Reason):") for e, c := range v.Fail.ServerErrorDist.Reasons { fmt.Fprintf(w, " %d\t :%s\n", c, e) } } fmt.Fprintln(w) } if s.result.TestStatus == "success" { fmt.Fprintf(w, "%s", green("Test Status : Success\n")) } else if s.result.TestStatus == "failed" { fmt.Fprintf(w, "\n%s", red("Test Status: Failed\n")) for _, failedRule := range s.result.TestFailedAssertions { fmt.Fprintf(w, red("\nRule: %s\n"), failedRule.Rule) fmt.Fprintf(w, red("Received: \n")) for ident, values := range failedRule.ReceivedMap { fmt.Fprintf(w, red("\t%s: %v\n"), ident, values) } } } w.Flush() fmt.Fprint(out, b.String()) } func deduplicate(values []interface{}) []interface{} { seen := make(map[interface{}]bool) result := make([]interface{}, 0) for _, v := range values { if _, ok := seen[v]; !ok { seen[v] = true result = append(result, v) } } return result } type duration struct { name string duration float32 order int } var keyToStr = map[string]duration{ "dnsDuration": {name: "DNS", order: 1}, "connDuration": {name: "Connection", order: 2}, "tlsDuration": {name: "TLS", order: 3}, "reqDuration": {name: "Request Write", order: 4}, "serverProcessDuration": {name: "Server Processing", order: 5}, "resDuration": {name: "Response Read", order: 6}, "duration": {name: "Total", order: 7}, } ================================================ FILE: ddosify_engine/core/report/stdoutJson.go ================================================ /* * * Ddosify - Load testing tool for any web system. * Copyright (C) 2021 Ddosify (https://ddosify.com) * * This program is free software: you can redistribute it and/or modify * it under the terms of the GNU Affero General Public License as published * by the Free Software Foundation, either version 3 of the License, or * (at your option) any later version. * * This program is distributed in the hope that it will be useful, * but WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * GNU Affero General Public License for more details. * * You should have received a copy of the GNU Affero General Public License * along with this program. If not, see . * */ package report import ( "encoding/json" "fmt" "io" "math" "sync" "time" "go.ddosify.com/ddosify/core/assertion" "go.ddosify.com/ddosify/core/types" ) const OutputTypeStdoutJson = "stdout-json" func init() { AvailableOutputServices[OutputTypeStdoutJson] = &stdoutJson{} } type stdoutJson struct { doneChan chan bool result *Result debug bool samplingRate int mu sync.Mutex } func (s *stdoutJson) Init(debug bool, samplingRate int) (err error) { s.doneChan = make(chan bool) s.result = &Result{ StepResults: make(map[uint16]*ScenarioStepResultSummary), } s.debug = debug s.samplingRate = samplingRate return } func (s *stdoutJson) Start(input chan *types.ScenarioResult, assertionResultChan <-chan assertion.TestAssertionResult) { if s.debug { s.result.TestStatus = "success" if assertionResultChan != nil { result := <-assertionResultChan if result.Fail { s.result.TestStatus = "failed" s.result.TestFailedAssertions = result.FailedRules } } s.printInDebugMode(input) if s.result.TestStatus == "success" { s.doneChan <- true } else { s.doneChan <- false } return } s.listenAndAggregate(input, assertionResultChan) s.report() if s.result.TestStatus == "success" { s.doneChan <- true } else { s.doneChan <- false } } func (s *stdoutJson) report() { p := 1e3 s.result.AvgDuration = float32(math.Round(float64(s.result.AvgDuration)*p) / p) for _, itemReport := range s.result.StepResults { durations := make(map[string]float32) for d, s := range itemReport.Durations { // Less precision for durations. t := math.Round(float64(s)*p) / p durations[strKeyToJsonKey[d]] = float32(t) } itemReport.Durations = durations } j, _ := json.Marshal(s.result) printJson(j) } func (s *stdoutJson) DoneChan() <-chan bool { return s.doneChan } func (s *stdoutJson) listenAndAggregate(input chan *types.ScenarioResult, assertionResultChan <-chan assertion.TestAssertionResult) { stopSampling := make(chan struct{}) samplingCount := make(map[uint16]map[string]int) go s.cleanSamplingCount(samplingCount, stopSampling, s.samplingRate) for r := range input { s.mu.Lock() // avoid race around samplingCount aggregate(s.result, r, samplingCount, s.samplingRate) s.mu.Unlock() } // listen for assertion result, add to json s.result.TestStatus = "success" if assertionResultChan != nil { result := <-assertionResultChan if result.Fail { s.result.TestStatus = "failed" s.result.TestFailedAssertions = result.FailedRules } } } func (s *stdoutJson) cleanSamplingCount(samplingCount map[uint16]map[string]int, stopSampling chan struct{}, samplingRate int) { ticker := time.NewTicker(1 * time.Second) for { select { case <-ticker.C: s.mu.Lock() // avoid race around samplingCount for stepId, ruleMap := range samplingCount { for rule, count := range ruleMap { if count >= samplingRate { samplingCount[stepId][rule] = 0 } } } s.mu.Unlock() case <-stopSampling: return } } } func (s *stdoutJson) printInDebugMode(input chan *types.ScenarioResult) { stepDebugResults := struct { DebugResults map[uint16]verboseHttpRequestInfo "json:\"steps\"" TestStatus string "json:\"test_status\"" TestFailedAssertions []assertion.FailedRule "json:\"failed_criterias,omitempty\"" }{ DebugResults: map[uint16]verboseHttpRequestInfo{}, } for r := range input { // only 1 sc ScenarioResult expected for _, sr := range r.StepResults { verboseInfo := ScenarioStepResultToVerboseHttpRequestInfo(sr) stepDebugResults.DebugResults[verboseInfo.StepId] = verboseInfo } } if s.result.TestStatus == "failed" { stepDebugResults.TestStatus = "failed" stepDebugResults.TestFailedAssertions = s.result.TestFailedAssertions } else { stepDebugResults.TestStatus = "success" } printPretty(out, stepDebugResults) } func printPretty(w io.Writer, info any) { valPretty, _ := json.MarshalIndent(info, "", " ") fmt.Fprintf(out, "%s \n", white(fmt.Sprintf(" %-6s", valPretty))) } // Report wraps Result to add success/fails percentage values type Report Result func (r Result) MarshalJSON() ([]byte, error) { return json.Marshal(struct { SuccesPerc int `json:"success_perc"` FailPerc int `json:"fail_perc"` Report }{ SuccesPerc: r.successPercentage(), FailPerc: r.failedPercentage(), Report: Report(r), }) } // ItemReport wraps ScenarioStepReport to add success/fails percentage values type ItemReport ScenarioStepResultSummary func (s ScenarioStepResultSummary) MarshalJSON() ([]byte, error) { return json.Marshal(struct { ItemReport SuccesPerc int `json:"success_perc"` FailPerc int `json:"fail_perc"` }{ ItemReport: ItemReport(s), SuccesPerc: s.successPercentage(), FailPerc: s.failedPercentage(), }) } var printJson = func(j []byte) { fmt.Println(string(j)) } var strKeyToJsonKey = map[string]string{ "dnsDuration": "dns", "connDuration": "connection", "tlsDuration": "tls", "reqDuration": "request_write", "serverProcessDuration": "server_processing", "resDuration": "response_read", "duration": "total", } func (v verboseHttpRequestInfo) MarshalJSON() ([]byte, error) { // could not prepare req, correlation if v.Error != "" && isVerboseInfoRequestEmpty(v.Request) { type alias struct { StepId uint16 `json:"step_id"` StepName string `json:"step_name"` Envs map[string]interface{} `json:"envs"` TestData map[string]interface{} `json:"test_data"` FailedCaptures map[string]string `json:"failed_captures"` FailedAssertions []types.FailedAssertion `json:"failed_assertions"` Error string `json:"error"` } a := alias{ Error: v.Error, StepId: v.StepId, StepName: v.StepName, FailedCaptures: v.FailedCaptures, FailedAssertions: v.FailedAssertions, Envs: v.Envs, TestData: v.TestData, } return json.Marshal(a) } if v.Error != "" { // server error no body type alias struct { StepId uint16 `json:"step_id"` StepName string `json:"step_name"` Envs map[string]interface{} `json:"envs"` TestData map[string]interface{} `json:"test_data"` FailedCaptures map[string]string `json:"failed_captures"` FailedAssertions []types.FailedAssertion `json:"failed_assertions"` Request struct { Url string `json:"url"` Method string `json:"method"` Headers map[string]string `json:"headers"` Body interface{} `json:"body"` } `json:"request"` Error string `json:"error"` } a := alias{ Request: v.Request, Error: v.Error, StepId: v.StepId, StepName: v.StepName, FailedCaptures: v.FailedCaptures, FailedAssertions: v.FailedAssertions, Envs: v.Envs, TestData: v.TestData, } return json.Marshal(a) } type alias struct { StepId uint16 `json:"step_id"` StepName string `json:"step_name"` Envs map[string]interface{} `json:"envs"` TestData map[string]interface{} `json:"test_data"` FailedCaptures map[string]string `json:"failed_captures"` FailedAssertions []types.FailedAssertion `json:"failed_assertions"` Request struct { Url string `json:"url"` Method string `json:"method"` Headers map[string]string `json:"headers"` Body interface{} `json:"body"` } `json:"request"` Response verboseResponse `json:"response"` } a := alias{ StepId: v.StepId, StepName: v.StepName, Request: v.Request, Response: v.Response, FailedCaptures: v.FailedCaptures, FailedAssertions: v.FailedAssertions, Envs: v.Envs, TestData: v.TestData, } return json.Marshal(a) } ================================================ FILE: ddosify_engine/core/report/stdoutJson_test.go ================================================ /* * * Ddosify - Load testing tool for any web system. * Copyright (C) 2021 Ddosify (https://ddosify.com) * * This program is free software: you can redistribute it and/or modify * it under the terms of the GNU Affero General Public License as published * by the Free Software Foundation, either version 3 of the License, or * (at your option) any later version. * * This program is distributed in the hope that it will be useful, * but WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * GNU Affero General Public License for more details. * * You should have received a copy of the GNU Affero General Public License * along with this program. If not, see . * */ package report import ( "bytes" "encoding/json" "io/ioutil" "os" "testing" "time" "go.ddosify.com/ddosify/core/assertion" "go.ddosify.com/ddosify/core/types" ) func TestInitStdoutJson(t *testing.T) { sj := &stdoutJson{} debug := false sj.Init(debug, 0) if sj.doneChan == nil { t.Errorf("DoneChan should be initialized") } if sj.result == nil { t.Errorf("Result map should be initialized") } } func TestStdoutJsonAggregate(t *testing.T) { responses := []*types.ScenarioResult{ { StartTime: time.Now(), StepResults: []*types.ScenarioStepResult{ { StepID: 1, StatusCode: 200, RequestTime: time.Now().Add(1), Duration: time.Duration(10) * time.Second, Custom: map[string]interface{}{ "dnsDuration": time.Duration(5) * time.Second, "connDuration": time.Duration(5) * time.Second, }, }, { StepID: 2, RequestTime: time.Now().Add(2), Duration: time.Duration(30) * time.Second, Err: types.RequestError{Type: types.ErrorConn, Reason: types.ReasonConnTimeout}, Custom: map[string]interface{}{ "dnsDuration": time.Duration(10) * time.Second, "connDuration": time.Duration(20) * time.Second, }, }, }, }, { StartTime: time.Now().Add(10), StepResults: []*types.ScenarioStepResult{ { StepID: 1, StatusCode: 200, RequestTime: time.Now().Add(11), Duration: time.Duration(30) * time.Second, Custom: map[string]interface{}{ "dnsDuration": time.Duration(10) * time.Second, "connDuration": time.Duration(20) * time.Second, }, }, { StepID: 2, StatusCode: 401, RequestTime: time.Now().Add(12), Duration: time.Duration(60) * time.Second, Custom: map[string]interface{}{ "dnsDuration": time.Duration(20) * time.Second, "connDuration": time.Duration(40) * time.Second, }, }, }, }, } fail1 := FailVerbose{} fail1.Count = 0 fail1.ServerErrorDist.Count = 0 fail1.ServerErrorDist.Reasons = make(map[string]int) fail1.AssertionErrorDist.Conditions = make(map[string]*AssertInfo) itemReport1 := &ScenarioStepResultSummary{ StatusCodeDist: map[int]int{200: 2}, SuccessCount: 2, Fail: fail1, Durations: map[string]float32{ "dnsDuration": 7.5, "connDuration": 12.5, "duration": 20, }, } fail2 := FailVerbose{} fail2.Count = 1 fail2.ServerErrorDist.Count = 1 fail2.ServerErrorDist.Reasons = make(map[string]int) fail2.ServerErrorDist.Reasons[types.ReasonConnTimeout] = 1 fail2.AssertionErrorDist.Conditions = make(map[string]*AssertInfo) itemReport2 := &ScenarioStepResultSummary{ StatusCodeDist: map[int]int{401: 1}, SuccessCount: 1, Fail: fail2, Durations: map[string]float32{ "dnsDuration": 20, "connDuration": 40, "duration": 60, }, } expectedResult := Result{ SuccessCount: 1, ServerFailedCount: 1, AvgDuration: 90, StepResults: map[uint16]*ScenarioStepResultSummary{ uint16(1): itemReport1, uint16(2): itemReport2, }, } s := &stdoutJson{} debug := false s.Init(debug, 0) for _, r := range responses { aggregate(s.result, r, make(map[uint16]map[string]int), 3) } if !compareResults(s.result, &expectedResult) { t.Errorf("Expected %#v, Found %#v", expectedResult, *s.result) } } func TestStdoutJsonOutput(t *testing.T) { // Arrange fail1 := FailVerbose{} fail1.Count = 0 fail1.ServerErrorDist.Count = 0 fail1.ServerErrorDist.Reasons = make(map[string]int) fail1.AssertionErrorDist.Conditions = make(map[string]*AssertInfo) itemReport1 := &ScenarioStepResultSummary{ StatusCodeDist: map[int]int{200: 11}, SuccessCount: 11, Fail: fail1, Durations: map[string]float32{ "dnsDuration": 0.1897, "connDuration": 0.0003, "duration": 0.1900, }, } fail2 := FailVerbose{} fail2.Count = 2 fail2.ServerErrorDist.Count = 2 fail2.ServerErrorDist.Reasons = make(map[string]int) fail2.ServerErrorDist.Reasons[types.ReasonConnTimeout] = 2 fail2.AssertionErrorDist.Conditions = make(map[string]*AssertInfo) itemReport2 := &ScenarioStepResultSummary{ StatusCodeDist: map[int]int{401: 1, 200: 9}, SuccessCount: 9, Fail: fail2, Durations: map[string]float32{ "dnsDuration": 0.48000, "connDuration": 0.01356, "duration": 0.493566, }, } result := Result{ SuccessCount: 9, ServerFailedCount: 2, AvgDuration: 0.25637, StepResults: map[uint16]*ScenarioStepResultSummary{ uint16(1): itemReport1, uint16(2): itemReport2, }, TestStatus: "success", } var output string printJson = func(j []byte) { output = string(j) } expectedOutputByte := []byte(`{ "success_perc": 81, "fail_perc": 19, "test_status": "success", "failed_criterias":null, "success_count": 9, "server_fail_count":2, "assertion_fail_count":0, "avg_duration": 0.256, "steps": { "1": { "name": "", "status_code_dist": { "200": 11 }, "fail":{ "count":0, "assertions": {"count":0,"conditions":{}}, "server": {"count":0,"reasons":{}} }, "durations": { "connection": 0, "dns": 0.19, "total": 0.19 }, "success_count": 11, "success_perc": 100, "fail_perc": 0 }, "2": { "name": "", "status_code_dist": { "200": 9, "401": 1 }, "fail":{ "count":2, "assertions": {"count":0,"conditions":{}}, "server": {"count":2,"reasons":{"connection timeout":2}} }, "durations": { "connection": 0.014, "dns": 0.48, "total": 0.494 }, "success_count": 9, "success_perc": 81, "fail_perc": 19 } } }`) buffer := new(bytes.Buffer) json.Compact(buffer, expectedOutputByte) expectedOutput := buffer.String() // Act s := &stdoutJson{result: &result} s.report() // Assert if output != expectedOutput { t.Errorf("Expected: %v, Found: %v", expectedOutput, output) } } func TestStdoutJsonDebugModePrintsValidJson(t *testing.T) { s := &stdoutJson{} s.Init(true, 0) testDoneChan := make(chan struct{}, 1) realOut := out r, w, _ := os.Pipe() out = w defer func() { out = realOut }() inputChan := make(chan *types.ScenarioResult, 1) inputChan <- &types.ScenarioResult{} close(inputChan) go func() { s.Start(inputChan, nil) w.Close() }() go func() { // wait for print and debug <-s.DoneChan() printedOutput, _ := ioutil.ReadAll(r) if !json.Valid(printedOutput) { t.Errorf("Printed output is not valid json: %v", string(printedOutput)) } testDoneChan <- struct{}{} }() <-testDoneChan } func TestVerboseHttpInfoMarshallingErrorCaseEmptyReq(t *testing.T) { errorStr := "there is error" vError := verboseHttpRequestInfo{ StepId: 0, StepName: "", Request: struct { Url string "json:\"url\"" Method string "json:\"method\"" Headers map[string]string "json:\"headers\"" Body interface{} "json:\"body\"" }{}, Error: errorStr, } bytesWithErrorAndNoResponse, _ := vError.MarshalJSON() var aliasStruct map[string]interface{} json.Unmarshal(bytesWithErrorAndNoResponse, &aliasStruct) val, errExists := aliasStruct["error"] _, respExists := aliasStruct["response"] _, requestExists := aliasStruct["request"] if !errExists { t.Errorf("Verbose Http Info should have error key") } else if val != errorStr { t.Errorf("Verbose Http Info should have error value as : %s, found: %s", errorStr, val) } else if respExists { t.Errorf("Verbose Http Info should not have response in case of error") } else if requestExists { t.Errorf("Verbose Http Info should not have request in case of empty req and error") } } func TestVerboseHttpInfoMarshallingErrorCase(t *testing.T) { errorStr := "there is error" vError := verboseHttpRequestInfo{ StepId: 0, StepName: "", Request: struct { Url string "json:\"url\"" Method string "json:\"method\"" Headers map[string]string "json:\"headers\"" Body interface{} "json:\"body\"" }{ Url: "", Method: "GET", Headers: map[string]string{}, Body: "some body", }, Error: errorStr, } bytesWithErrorAndNoResponse, _ := vError.MarshalJSON() var aliasStruct map[string]interface{} json.Unmarshal(bytesWithErrorAndNoResponse, &aliasStruct) val, errExists := aliasStruct["error"] _, respExists := aliasStruct["response"] if !errExists { t.Errorf("Verbose Http Info should have error key") } else if val != errorStr { t.Errorf("Verbose Http Info should have error value as : %s, found: %s", errorStr, val) } else if respExists { t.Errorf("Verbose Http Info should not have response in case of error") } } func TestVerboseHttpInfoMarshallingSuccessCase(t *testing.T) { noErrorStr := "" vSuccess := verboseHttpRequestInfo{ StepId: 0, StepName: "", Request: struct { Url string "json:\"url\"" Method string "json:\"method\"" Headers map[string]string "json:\"headers\"" Body interface{} "json:\"body\"" }{}, Response: verboseResponse{}, Error: noErrorStr, } bytesWithResponseAndNoError, _ := vSuccess.MarshalJSON() var aliasStruct map[string]interface{} json.Unmarshal(bytesWithResponseAndNoError, &aliasStruct) _, errExists := aliasStruct["error"] _, respExists := aliasStruct["response"] if errExists { t.Errorf("Verbose Http Info should not have error key in success case") } else if !respExists { t.Errorf("Verbose Http Info should have response in success case") } } func TestStdoutJsonTestResultStatusShouldBeTrueWhenNoAssertion(t *testing.T) { s := &stdoutJson{} s.Init(false, 0) inputChan := make(chan *types.ScenarioResult, 1) inputChan <- &types.ScenarioResult{} close(inputChan) go func() { s.Start(inputChan, nil) }() testStatus := <-s.DoneChan() if !testStatus { t.Errorf("Test status should be true") } } func TestStdoutJsonTestResultStatusShouldBeFalseWhenAssertionsFail(t *testing.T) { s := &stdoutJson{} s.Init(false, 0) inputChan := make(chan *types.ScenarioResult, 1) inputChan <- &types.ScenarioResult{} close(inputChan) assertionResultChan := make(chan assertion.TestAssertionResult, 1) assertionResultChan <- assertion.TestAssertionResult{ Fail: true, } go func() { s.Start(inputChan, assertionResultChan) }() testStatus := <-s.DoneChan() if testStatus { t.Errorf("Test status should be false") } } ================================================ FILE: ddosify_engine/core/report/stdout_test.go ================================================ /* * * Ddosify - Load testing tool for any web system. * Copyright (C) 2021 Ddosify (https://ddosify.com) * * This program is free software: you can redistribute it and/or modify * it under the terms of the GNU Affero General Public License as published * by the Free Software Foundation, either version 3 of the License, or * (at your option) any later version. * * This program is distributed in the hope that it will be useful, * but WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * GNU Affero General Public License for more details. * * You should have received a copy of the GNU Affero General Public License * along with this program. If not, see . * */ package report import ( "bytes" "encoding/json" "io/ioutil" "os" "strings" "testing" "time" "go.ddosify.com/ddosify/core/types" ) //TODO:move aggregator.go related tests cases to aggregator_test.go func TestScenarioStepReport(t *testing.T) { tests := []struct { name string s ScenarioStepResultSummary successPercentage int failedPercentage int }{ {"S:0-SF:0-AF:0", ScenarioStepResultSummary{SuccessCount: 0, Fail: FailVerbose{Count: 0, ServerErrorDist: ServerErrVerbose{Count: 0}, AssertionErrorDist: AssertionErrVerbose{Count: 0}}}, 0, 0}, {"S:0-SF:1-AF:0", ScenarioStepResultSummary{SuccessCount: 0, Fail: FailVerbose{Count: 1, ServerErrorDist: ServerErrVerbose{Count: 1}, AssertionErrorDist: AssertionErrVerbose{Count: 0}}}, 0, 100}, {"S:1-SF:0-AF:0", ScenarioStepResultSummary{SuccessCount: 1, Fail: FailVerbose{Count: 0, ServerErrorDist: ServerErrVerbose{Count: 0}, AssertionErrorDist: AssertionErrVerbose{Count: 0}}}, 100, 0}, {"S:3-SF:9-AF:6", ScenarioStepResultSummary{SuccessCount: 3, Fail: FailVerbose{Count: 9, ServerErrorDist: ServerErrVerbose{Count: 3}, AssertionErrorDist: AssertionErrVerbose{Count: 6}}}, 25, 75}, {"S:5-SF:2-AF:3", ScenarioStepResultSummary{SuccessCount: 5, Fail: FailVerbose{Count: 5, ServerErrorDist: ServerErrVerbose{Count: 2}, AssertionErrorDist: AssertionErrVerbose{Count: 3}}}, 50, 50}, } for _, test := range tests { tf := func(t *testing.T) { sp := test.s.successPercentage() fp := test.s.failedPercentage() if test.successPercentage != sp { t.Errorf("SuccessPercentage Expected %d Found %d", test.successPercentage, sp) } if test.failedPercentage != fp { t.Errorf("FailedPercentage Expected %d Found %d", test.failedPercentage, fp) } } t.Run(test.name, tf) } } func TestResult(t *testing.T) { tests := []struct { name string r Result successPercentage int failedPercentage int }{ {"S:0-F:0", Result{ServerFailedCount: 0, SuccessCount: 0}, 0, 0}, {"S:0-F:1", Result{ServerFailedCount: 1, SuccessCount: 0}, 0, 100}, {"S:1-F:0", Result{ServerFailedCount: 0, SuccessCount: 1}, 100, 0}, {"S:3-F:9", Result{ServerFailedCount: 9, SuccessCount: 3}, 25, 75}, } for _, test := range tests { tf := func(t *testing.T) { sp := test.r.successPercentage() fp := test.r.failedPercentage() if test.successPercentage != sp { t.Errorf("SuccessPercentage Expected %d Found %d", test.successPercentage, sp) } if test.failedPercentage != fp { t.Errorf("FailedPercentage Expected %d Found %d", test.failedPercentage, fp) } } t.Run(test.name, tf) } } func TestInit(t *testing.T) { s := &stdout{} debug := false s.Init(debug, 0) if s.doneChan == nil { t.Errorf("DoneChan should be initialized") } if s.result == nil { t.Errorf("Result map should be initialized") } } func TestPrintJsonBody(t *testing.T) { var byteArr []byte buffer := bytes.NewBuffer(byteArr) contentTypeJson := "application/json" body := map[string]interface{}{"x": "y"} printBody(buffer, contentTypeJson, body) printedBody := buffer.Bytes() if !json.Valid(printedBody) { t.Errorf("Printed body is not valid json: %v", string(printedBody)) } } func TestPrintBodyAsString(t *testing.T) { var byteArr []byte buffer := bytes.NewBuffer(byteArr) contentTypeAny := "any" body := "argentina" printBody(buffer, contentTypeAny, body) printedBody := buffer.Bytes() if !strings.Contains(string(printedBody), body) { t.Errorf("Printed body does not match expected: %s, found: %v", body, string(printedBody)) } } func TestStdoutPrintsHeadlinesInDebugMode(t *testing.T) { s := &stdout{} s.Init(true, 0) testDoneChan := make(chan struct{}, 1) // listen to output realOut := out r, w, _ := os.Pipe() out = w defer func() { out = realOut }() inputChan := make(chan *types.ScenarioResult, 1) inputChan <- &types.ScenarioResult{ StepResults: []*types.ScenarioStepResult{ { StepID: 0, StepName: "", RequestID: [16]byte{}, StatusCode: 0, RequestTime: time.Time{}, Duration: 0, ContentLength: 0, Err: types.RequestError{}, Custom: map[string]interface{}{}, }, }, } close(inputChan) go func() { s.Start(inputChan, nil) w.Close() }() go func() { // wait for print and debug <-s.DoneChan() printedOutput, err := ioutil.ReadAll(r) t.Log(err) t.Log(printedOutput) outStr := string(printedOutput) if !strings.Contains(outStr, "- Request") || !strings.Contains(outStr, "Headers:") || !strings.Contains(outStr, "Body:") || !strings.Contains(outStr, "- Response") || !strings.Contains(outStr, "StatusCode:") { t.Errorf("One or multiple headlines are missing in stdout debug mode") } testDoneChan <- struct{}{} }() <-testDoneChan } ================================================ FILE: ddosify_engine/core/scenario/client_pool.go ================================================ package scenario import ( "errors" "net/http" "net/http/cookiejar" "net/url" "go.ddosify.com/ddosify/core/types" "go.ddosify.com/ddosify/core/util" ) // Factory is a function to create new connections. type ClientFactoryMethod func() *http.Client type ClientCloseMethod func(*http.Client) // NewClientPool returns a new pool based on buffered channels with an initial // capacity and maximum capacity. Factory is used when initial capacity is // greater than zero to fill the pool. A zero initialCap doesn't fill the Pool // until a new Get() is called. During a Get(), If there is no new client // available in the pool, a new client will be created via the Factory() // method. func NewClientPool(initialCap, maxCap int, engineMode string, factory ClientFactoryMethod, close ClientCloseMethod) (*util.Pool[*http.Client], error) { if initialCap < 0 || maxCap <= 0 || initialCap > maxCap { return nil, errors.New("invalid capacity settings") } pool := &util.Pool[*http.Client]{ Items: make(chan *http.Client, maxCap), Factory: factory, Close: close, AfterPut: func(client *http.Client) { // if engine is in repeated mode, notify jar that cookies are already set // to avoid setting them again in the next iteration if engineMode == types.EngineModeRepeatedUser && client.Jar != nil && !client.Jar.(*cookieJarRepeated).firstIterPassed { client.Jar.(*cookieJarRepeated).firstIterPassed = true } }, } // create initial clients, if something goes wrong, // just close the pool error out. for i := 0; i < initialCap; i++ { client := pool.Factory() pool.Items <- client } return pool, nil } type cookieJarRepeated struct { defaultCookieJar *cookiejar.Jar firstIterPassed bool } func NewCookieJarRepeated() (*cookieJarRepeated, error) { jar, err := cookiejar.New(nil) if err != nil { return nil, err } return &cookieJarRepeated{defaultCookieJar: jar}, nil } // SetCookies implements the http.CookieJar interface. // Only set cookies if they are not already set for repeated mode. func (c *cookieJarRepeated) SetCookies(u *url.URL, cookies []*http.Cookie) { if !c.firstIterPassed { // execute default behavior if no cookies are set c.defaultCookieJar.SetCookies(u, cookies) } } // Cookies implements the http.CookieJar interface. func (c *cookieJarRepeated) Cookies(u *url.URL) []*http.Cookie { return c.defaultCookieJar.Cookies(u) } var defaultFactory = func() *http.Client { return &http.Client{} } var defaultClose = func(c *http.Client) { c.CloseIdleConnections() } // createClientFactoryMethod returns a Factory function based on the engine mode. func createClientFactoryMethod(mode string, opts ...func(http.CookieJar)) ClientFactoryMethod { if mode == types.EngineModeRepeatedUser { return func() *http.Client { jar, err := NewCookieJarRepeated() if err != nil { return defaultFactory() // no cookie jar, use default factory } for _, opt := range opts { opt(jar) } return &http.Client{Jar: jar} } } // distinct users mode return func() *http.Client { jar, err := cookiejar.New(nil) if err != nil { return defaultFactory() // no cookie jar, use default factory } for _, opt := range opts { opt(jar) } return &http.Client{Jar: jar} } } ================================================ FILE: ddosify_engine/core/scenario/client_pool_cookie_test.go ================================================ package scenario import ( "net/http" "net/http/cookiejar" "net/http/httptest" "net/url" "testing" "go.ddosify.com/ddosify/core/types" ) // If the user agent receives a new cookie with the same cookie-name, // domain-value, and path-value as a cookie that it has already stored, // the existing cookie is evicted and replaced with the new cookie func TestCookieManagerInRepeatedModeOnlySetInFirstIter(t *testing.T) { t.Parallel() cookieName := "test" // cookie value sent by server (first step) value1 := "test1" value2 := "test2" // login endpoint will set this value to cookie in second iteration, but we expect it to be ignored // cookies sent to second step var cookieInFirstCall string var cookieInSecondCall string loginCallCount := 0 orderCallCount := 0 firstReqHandler := func(w http.ResponseWriter, r *http.Request) { // set cookie, act as server var val string if loginCallCount == 0 { val = value1 } else { val = value2 } cookie := http.Cookie{Name: cookieName, Value: val} http.SetCookie(w, &cookie) loginCallCount++ } secondReqHandler := func(w http.ResponseWriter, r *http.Request) { // check cookie sent by client ck, _ := r.Cookie(cookieName) if orderCallCount == 0 { cookieInFirstCall = ck.Value } else { cookieInSecondCall = ck.Value } orderCallCount++ } pathFirst := "/login" pathSecond := "/order" mux := http.NewServeMux() mux.HandleFunc(pathFirst, firstReqHandler) mux.HandleFunc(pathSecond, secondReqHandler) host := httptest.NewServer(mux) defer host.Close() // make sure we get the same client in second iteration, 1,1 means we have only one client pool, _ := NewClientPool(1, 1, types.EngineModeRepeatedUser, createClientFactoryMethod(types.EngineModeRepeatedUser), defaultClose) c := pool.Get() // using same client // first iteration c.Get(host.URL + pathFirst) c.Get(host.URL + pathSecond) // put client back to pool, so we can reuse it in second iteration pool.Put(c) c = pool.Get() // second iteration c.Get(host.URL + pathFirst) c.Get(host.URL + pathSecond) if cookieInFirstCall != cookieInSecondCall { t.Errorf("TestCookieManagerInRepeatedModeOnlySetInFirstIter, cookie should be same in second iteration") } } func TestSetCookiesAppendToCurrentSliceOfCookies(t *testing.T) { jar, _ := cookiejar.New(nil) cookie1 := &http.Cookie{Name: "test1", Value: "test1"} cookie2 := &http.Cookie{Name: "test2", Value: "test2"} // set cookie url := url.URL{Scheme: "http", Host: "test.com"} jar.SetCookies(&url, []*http.Cookie{cookie1}) jar.SetCookies(&url, []*http.Cookie{cookie2}) cookies := jar.Cookies(&url) if len(cookies) != 2 { t.Errorf("TestCookieSetOverrides, expected 2 cookies, got %d", len(cookies)) } } func TestSetCookiesOverridesCookieWithSameName(t *testing.T) { jar, _ := cookiejar.New(nil) cookie1 := &http.Cookie{Name: "test", Value: "test1"} cookie2 := &http.Cookie{Name: "test", Value: "test2"} // set cookie url := url.URL{Scheme: "http", Host: "test.com"} jar.SetCookies(&url, []*http.Cookie{cookie1}) jar.SetCookies(&url, []*http.Cookie{cookie2}) cookies := jar.Cookies(&url) if len(cookies) != 1 || cookies[0].Value != "test2" { t.Errorf("TestSetCookiesOverridesCookieWithSameName, expected 1 cookie with value 'test2', got %d", len(cookies)) } } func TestSetCookiesDeletesIfUnnecessary(t *testing.T) { jar, _ := cookiejar.New(nil) cookie1 := &http.Cookie{Name: "test", Value: "test1", Secure: false} cookie2 := &http.Cookie{Name: "test", Value: "test2", Secure: true} // set cookie url := url.URL{Scheme: "http", Host: "test.com"} jar.SetCookies(&url, []*http.Cookie{cookie1}) cookies := jar.Cookies(&url) if len(cookies) != 1 { t.Errorf("TestSetCookiesDeletesIfUnnecessary, expected 1 cookie with value 'test1', got %d", len(cookies)) } jar.SetCookies(&url, []*http.Cookie{cookie2}) cookies = jar.Cookies(&url) // cookiejar deletes cookies with same name if url scheme is http and cookie is secure if len(cookies) != 0 { t.Errorf("TestSetCookiesDeletesIfUnnecessary, expected 1 cookie with value 'test2', got %d", len(cookies)) } } func TestSetCookiesUrlScheme(t *testing.T) { jar, _ := cookiejar.New(nil) cookie1 := &http.Cookie{Name: "test1", Value: "test1"} cookie2 := &http.Cookie{Name: "test2", Value: "test2"} // set cookie url := url.URL{Scheme: "", Host: "test.com"} // expect set cookies to be ignored since url scheme is empty jar.SetCookies(&url, []*http.Cookie{cookie1}) jar.SetCookies(&url, []*http.Cookie{cookie2}) cookies := jar.Cookies(&url) if len(cookies) != 0 { t.Errorf("TestSetCookiesUrlScheme, expected 0 cookies, got %d", len(cookies)) } } func TestSetCookiesSecure(t *testing.T) { t.Parallel() secureCookieName := "https-cookie" secureCookieVal := "secure-cookie" var httpServerGotCookie *http.Cookie reqHandler := func(w http.ResponseWriter, r *http.Request) { httpServerGotCookie, _ = r.Cookie(secureCookieName) } var httpsServerGotCookie *http.Cookie secureReqHandler := func(w http.ResponseWriter, r *http.Request) { httpsServerGotCookie, _ = r.Cookie(secureCookieName) } path := "/default" mux := http.NewServeMux() mux.HandleFunc(path, reqHandler) host := httptest.NewServer(mux) defer host.Close() pathSecure := "/secure" muxHttps := http.NewServeMux() muxHttps.HandleFunc(pathSecure, secureReqHandler) secureHost := httptest.NewTLSServer(muxHttps) defer secureHost.Close() c := secureHost.Client() c.Jar, _ = cookiejar.New(nil) secureCookie := http.Cookie{Name: secureCookieName, Value: secureCookieVal, Secure: true} // set cookies url, _ := url.Parse(secureHost.URL) c.Jar.SetCookies(url, []*http.Cookie{&secureCookie}) c.Get(host.URL + path) c.Get(secureHost.URL + pathSecure) // expect secure cookie to be sent only to secure host if httpServerGotCookie != nil { t.Errorf("TestSetCookiesSecure, expected no cookie to be sent to http host, got %s", httpServerGotCookie.Value) } if httpsServerGotCookie == nil || httpsServerGotCookie.Value != secureCookieVal { t.Errorf("TestSetCookiesSecure, expected cookie to be sent to https host, got %s", httpsServerGotCookie.Value) } } func TestPutInitialCookiesInJarFactory(t *testing.T) { mode := types.EngineModeDistinctUser pool, _ := NewClientPool(1, 1, mode, putInitialCookiesInJarFactory(mode, []*http.Cookie{ { Name: "test", Value: "test", Domain: "ddosify.com", Secure: false, }, { Name: "test", Value: "test", Domain: "servdown.com", Secure: true, }}), defaultClose) c := pool.Get() cookies := c.Jar.Cookies(&url.URL{Scheme: "http", Host: "ddosify.com"}) if len(cookies) != 1 { t.Errorf("TestPutInitialCookiesInJarFactory, expected 1 cookie, got %d", len(cookies)) } if cookies[0].Value != "test" { t.Errorf("TestPutInitialCookiesInJarFactory, expected cookie value 'test', got %s", cookies[0].Value) } cookies = c.Jar.Cookies(&url.URL{Scheme: "https", Host: "servdown.com"}) if len(cookies) != 1 { t.Errorf("TestPutInitialCookiesInJarFactory, expected 1 cookie, got %d", len(cookies)) } if cookies[0].Value != "test" { t.Errorf("TestPutInitialCookiesInJarFactory, expected cookie value 'test', got %s", cookies[0].Value) } } ================================================ FILE: ddosify_engine/core/scenario/data/csv.go ================================================ package data import ( "encoding/csv" "encoding/json" "fmt" "io" "net/http" "net/url" "os" "strconv" "go.ddosify.com/ddosify/core/types" ) func validateConf(conf types.CsvConf) error { if !(conf.Order == "random" || conf.Order == "sequential") { return fmt.Errorf("unsupported order %s, should be random|sequential", conf.Order) } return nil } type RemoteCsvError struct { // UnWrappable msg string wrappedErr error } func (nf RemoteCsvError) Error() string { if nf.wrappedErr != nil { return fmt.Sprintf("%s,%s", nf.msg, nf.wrappedErr.Error()) } return nf.msg } func (nf RemoteCsvError) Unwrap() error { return nf.wrappedErr } func ReadCsv(conf types.CsvConf) ([]map[string]interface{}, error) { err := validateConf(conf) if err != nil { return nil, err } var reader io.Reader var pUrl *url.URL if pUrl, err = url.ParseRequestURI(conf.Path); err == nil && pUrl.IsAbs() { // url req, err := http.NewRequest(http.MethodGet, conf.Path, nil) if err != nil { return nil, wrapAsCsvError("can not create request", err) } resp, err := http.DefaultClient.Do(req) if err != nil { return nil, wrapAsCsvError("can not get response", err) } if !(resp.StatusCode >= 200 && resp.StatusCode <= 299) { return nil, wrapAsCsvError(fmt.Sprintf("Test Data: request to remote url (%s) failed. Status Code: %d", conf.Path, resp.StatusCode), nil) } reader = resp.Body defer resp.Body.Close() } else if _, err = os.Stat(conf.Path); err == nil { // local file path f, err := os.Open(conf.Path) if err != nil { return nil, err } reader = f defer f.Close() } else { return nil, wrapAsCsvError(fmt.Sprintf("can not parse path: %s", conf.Path), err) } // read csv values using csv.Reader csvReader := csv.NewReader(reader) csvReader.Comma = []rune(conf.Delimiter)[0] csvReader.TrimLeadingSpace = true csvReader.LazyQuotes = conf.AllowQuota data, err := csvReader.ReadAll() if err != nil { return nil, err } if conf.SkipFirstLine { data = data[1:] } rt := make([]map[string]interface{}, 0) // unclear how many empty line exist for _, row := range data { if conf.SkipEmptyLine && emptyLine(row) { continue } x := map[string]interface{}{} for index, tag := range conf.Vars { i, err := strconv.Atoi(index) if err != nil { return nil, err } if i >= len(row) { return nil, fmt.Errorf("index number out of range, check your vars or delimiter") } // convert var val interface{} switch tag.Type { case "json": err := json.Unmarshal([]byte(row[i]), &val) if err != nil { return nil, fmt.Errorf("can not convert %s to json,%v", row[i], err) } case "int": var err error val, err = strconv.Atoi(row[i]) if err != nil { return nil, fmt.Errorf("can not convert %s to int,%v", row[i], err) } case "float": var err error val, err = strconv.ParseFloat(row[i], 64) if err != nil { return nil, fmt.Errorf("can not convert %s to float,%v", row[i], err) } case "bool": var err error val, err = strconv.ParseBool(row[i]) if err != nil { return nil, fmt.Errorf("can not convert %s to bool,%v", row[i], err) } default: val = row[i] } x[tag.Tag] = val } rt = append(rt, x) } return rt, nil } func emptyLine(row []string) bool { for _, field := range row { if field != "" { return false } } return true } func wrapAsCsvError(msg string, err error) RemoteCsvError { var csvReqError RemoteCsvError csvReqError.msg = msg csvReqError.wrappedErr = err return csvReqError } ================================================ FILE: ddosify_engine/core/scenario/data/csv_test.go ================================================ package data import ( "errors" "fmt" "net/http" "net/http/httptest" "reflect" "strings" "testing" "go.ddosify.com/ddosify/core/types" ) func TestValidateCsvConf(t *testing.T) { t.Parallel() conf := types.CsvConf{ Path: "", Delimiter: "", SkipFirstLine: false, Vars: map[string]types.Tag{}, SkipEmptyLine: false, AllowQuota: false, Order: "", } conf.Order = "invalidOrder" err := validateConf(conf) if err == nil { t.Errorf("TestValidateCsvConf should be errored") } } func TestReadCsv_RemoteErr(t *testing.T) { t.Parallel() conf := types.CsvConf{ Path: "https://invalidurl.com/csv", Delimiter: ";", SkipFirstLine: true, Vars: map[string]types.Tag{ "0": {Tag: "name", Type: "string"}, "3": {Tag: "payload", Type: "json"}, "4": {Tag: "age", Type: "int"}, "5": {Tag: "percent", Type: "float"}, "6": {Tag: "boolField", Type: "bool"}, }, SkipEmptyLine: true, AllowQuota: true, Order: "sequential", } _, err := ReadCsv(conf) if err == nil { t.Errorf("TestReadCsv_RemoteErr %v", err) } var remoteCsvErr RemoteCsvError if !errors.As(err, &remoteCsvErr) { t.Errorf("Expected: %v, Found: %v", remoteCsvErr, err) } if remoteCsvErr.Unwrap() == nil { t.Errorf("Expected: %v, Found: %v", "not nil", remoteCsvErr.Unwrap()) } } func TestWrapAsRemoteCsvError(t *testing.T) { msg := "xxyy" csvErr := wrapAsCsvError(msg, fmt.Errorf("error")) var remoteCsvErr RemoteCsvError if !errors.As(csvErr, &remoteCsvErr) { t.Errorf("Expected: %v, Found: %v", remoteCsvErr, csvErr) } errmsg := remoteCsvErr.Error() if errmsg != msg+",error" { t.Errorf("Expected: %v, Found: %v", msg, remoteCsvErr.msg) } } func TestReadCsvFromRemote(t *testing.T) { // Test server handler := func(w http.ResponseWriter, r *http.Request) { w.WriteHeader(http.StatusUnauthorized) } path := "/csv" mux := http.NewServeMux() mux.HandleFunc(path, handler) server := httptest.NewServer(mux) defer server.Close() conf := types.CsvConf{ Path: server.URL + path, Delimiter: ";", SkipFirstLine: true, Vars: map[string]types.Tag{ "0": {Tag: "name", Type: "string"}, "3": {Tag: "payload", Type: "json"}, "4": {Tag: "age", Type: "int"}, "5": {Tag: "percent", Type: "float"}, "6": {Tag: "boolField", Type: "bool"}, }, SkipEmptyLine: true, AllowQuota: true, Order: "sequential", } _, err := ReadCsv(conf) if err == nil { t.Errorf("TestReadCsvFromRemote %v", err) } var remoteCsvErr RemoteCsvError if !errors.As(err, &remoteCsvErr) { t.Errorf("Expected: %v, Found: %v", remoteCsvErr, err) } } func TestReadCsv(t *testing.T) { t.Parallel() conf := types.CsvConf{ Path: "../../../config/config_testdata/test.csv", Delimiter: ";", SkipFirstLine: true, Vars: map[string]types.Tag{ "0": {Tag: "name", Type: "string"}, "3": {Tag: "payload", Type: "json"}, "4": {Tag: "age", Type: "int"}, "5": {Tag: "percent", Type: "float"}, "6": {Tag: "boolField", Type: "bool"}, }, SkipEmptyLine: true, AllowQuota: true, Order: "sequential", } rows, err := ReadCsv(conf) if err != nil { t.Errorf("TestReadCsv %v", err) } firstName := rows[0]["name"].(string) expectedName := "Kenan" if !strings.EqualFold(firstName, expectedName) { t.Errorf("TestReadCsv found: %s , expected: %s", firstName, expectedName) } firstAge := rows[0]["age"].(int) expectedAge := 25 if firstAge != expectedAge { t.Errorf("TestReadCsv found: %d , expected: %d", firstAge, expectedAge) } firstPercent := rows[0]["percent"].(float64) expectedPercent := 22.3 if firstPercent != expectedPercent { t.Errorf("TestReadCsv found: %f , expected: %f", firstPercent, expectedPercent) } firstBool := rows[0]["boolField"].(bool) expectedBool := true if firstBool != expectedBool { t.Errorf("TestReadCsv found: %t , expected: %t", firstBool, expectedBool) } firstPayload := rows[0]["payload"].(map[string]interface{}) expectedPayload := map[string]interface{}{ "data": map[string]interface{}{ "profile": map[string]interface{}{ "name": "Kenan", }, }, } if !reflect.DeepEqual(firstPayload, expectedPayload) { t.Errorf("TestReadCsv found: %#v , expected: %#v", firstPayload, expectedPayload) } secondPayload := rows[1]["payload"].([]interface{}) expectedPayload2 := []interface{}{5.0, 6.0, 7.0} // underlying type float64 if !reflect.DeepEqual(secondPayload, expectedPayload2) { t.Errorf("TestReadCsv found: %#v , expected: %#v", secondPayload, expectedPayload2) } } var table = []struct { conf types.CsvConf latency float64 }{ { conf: types.CsvConf{ Path: "config_testdata/test.csv", Delimiter: ";", SkipFirstLine: true, Vars: map[string]types.Tag{ "0": {Tag: "name", Type: "string"}, "3": {Tag: "payload", Type: "json"}, "4": {Tag: "age", Type: "int"}, "5": {Tag: "percent", Type: "float"}, "6": {Tag: "boolField", Type: "bool"}, }, SkipEmptyLine: true, AllowQuota: true, Order: "sequential", }, }, } func TestBenchmarkCsvRead(t *testing.T) { for _, v := range table { res := testing.Benchmark(func(b *testing.B) { for i := 0; i < b.N; i++ { ReadCsv(v.conf) } }) fmt.Printf("ns:%d", res.T.Nanoseconds()) fmt.Printf("N:%d", res.N) } } ================================================ FILE: ddosify_engine/core/scenario/requester/base.go ================================================ /* * * Ddosify - Load testing tool for any web system. * Copyright (C) 2021 Ddosify (https://ddosify.com) * * This program is free software: you can redistribute it and/or modify * it under the terms of the GNU Affero General Public License as published * by the Free Software Foundation, either version 3 of the License, or * (at your option) any later version. * * This program is distributed in the hope that it will be useful, * but WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * GNU Affero General Public License for more details. * * You should have received a copy of the GNU Affero General Public License * along with this program. If not, see . * */ package requester import ( "context" "net/http" "net/url" "go.ddosify.com/ddosify/core/scenario/scripting/injection" "go.ddosify.com/ddosify/core/types" ) // Requester is the interface that abstracts different protocols' request sending implementations. // Protocol field in the types.ScenarioStep determines which requester implementation to use. type Requester interface { Type() string Done() } type HttpRequesterI interface { Init(ctx context.Context, ss types.ScenarioStep, url *url.URL, debug bool, ei *injection.EnvironmentInjector) error Send(client *http.Client, envs map[string]interface{}) *types.ScenarioStepResult // should use its own client if client is nil } // NewRequester is the factory method of the Requester. func NewRequester(s types.ScenarioStep) (requester Requester, err error) { requester = &HttpRequester{} // we have only HttpRequester type for now, add check for rpc in future return } ================================================ FILE: ddosify_engine/core/scenario/requester/base_test.go ================================================ /* * * Ddosify - Load testing tool for any web system. * Copyright (C) 2021 Ddosify (https://ddosify.com) * * This program is free software: you can redistribute it and/or modify * it under the terms of the GNU Affero General Public License as published * by the Free Software Foundation, either version 3 of the License, or * (at your option) any later version. * * This program is distributed in the hope that it will be useful, * but WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * GNU Affero General Public License for more details. * * You should have received a copy of the GNU Affero General Public License * along with this program. If not, see . * */ package requester import ( "reflect" "go.ddosify.com/ddosify/core/types" ) var protocolStrategiesStructMap = map[string]reflect.Type{ types.ProtocolHTTP: reflect.TypeOf(&HttpRequester{}), types.ProtocolHTTPS: reflect.TypeOf(&HttpRequester{}), } ================================================ FILE: ddosify_engine/core/scenario/requester/http.go ================================================ /* * * Ddosify - Load testing tool for any web system. * Copyright (C) 2021 Ddosify (https://ddosify.com) * * This program is free software: you can redistribute it and/or modify * it under the terms of the GNU Affero General Public License as published * by the Free Software Foundation, either version 3 of the License, or * (at your option) any later version. * * This program is distributed in the hope that it will be useful, * but WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * GNU Affero General Public License for more details. * * You should have received a copy of the GNU Affero General Public License * along with this program. If not, see . * */ package requester import ( "bytes" "context" "crypto/tls" "errors" "fmt" "io" "net/http" "net/http/httptrace" "net/url" "regexp" "strconv" "strings" "sync" "time" "github.com/google/uuid" "go.ddosify.com/ddosify/core/scenario/scripting/assertion" "go.ddosify.com/ddosify/core/scenario/scripting/assertion/evaluator" "go.ddosify.com/ddosify/core/scenario/scripting/extraction" "go.ddosify.com/ddosify/core/scenario/scripting/injection" "go.ddosify.com/ddosify/core/types" "go.ddosify.com/ddosify/core/types/regex" "golang.org/x/net/http2" ) type HttpRequester struct { ctx context.Context proxyAddr *url.URL packet types.ScenarioStep client *http.Client request *http.Request ei *injection.EnvironmentInjector containsDynamicField map[string]bool containsEnvVar map[string]bool debug bool dynamicRgx *regexp.Regexp envRgx *regexp.Regexp } // Init creates a client with the given scenarioItem. HttpRequester uses the same http.Client for all requests func (h *HttpRequester) Init(ctx context.Context, s types.ScenarioStep, proxyAddr *url.URL, debug bool, ei *injection.EnvironmentInjector) (err error) { h.ctx = ctx h.packet = s h.proxyAddr = proxyAddr h.ei = ei h.containsDynamicField = make(map[string]bool) h.containsEnvVar = make(map[string]bool) h.debug = debug h.dynamicRgx = regexp.MustCompile(regex.DynamicVariableRegex) h.envRgx = regexp.MustCompile(regex.EnvironmentVariableRegex) // Transport segment tr := h.initTransport() tr.MaxIdleConnsPerHost = 60000 tr.MaxIdleConns = 0 // http client h.client = &http.Client{Transport: tr, Timeout: time.Duration(h.packet.Timeout) * time.Second} if val, ok := h.packet.Custom["disable-redirect"]; ok { val := val.(bool) if val { h.client.CheckRedirect = func(req *http.Request, via []*http.Request) error { return http.ErrUseLastResponse } } } // Request instance err = h.initRequestInstance() if err != nil { return } // body if h.dynamicRgx.MatchString(h.packet.Payload) { _, err = h.ei.InjectDynamic(h.packet.Payload) if err != nil { return } h.containsDynamicField["body"] = true } if h.envRgx.MatchString(h.packet.Payload) { h.containsEnvVar["body"] = true } // url if h.dynamicRgx.MatchString(h.packet.URL) { _, err = h.ei.InjectDynamic(h.packet.URL) if err != nil { return } h.containsDynamicField["url"] = true } if h.envRgx.MatchString(h.packet.URL) { h.containsEnvVar["url"] = true } // header for k, values := range h.request.Header { for _, v := range values { if h.dynamicRgx.MatchString(k) || h.dynamicRgx.MatchString(v) { _, err = h.ei.InjectDynamic(k) if err != nil { return } _, err = h.ei.InjectDynamic(v) if err != nil { return } h.containsDynamicField["header"] = true } if h.envRgx.MatchString(k) || h.envRgx.MatchString(v) { h.containsEnvVar["header"] = true } } } // basicauth if h.dynamicRgx.MatchString(h.packet.Auth.Username) || h.dynamicRgx.MatchString(h.packet.Auth.Password) { _, err = h.ei.InjectDynamic(h.packet.Auth.Username) if err != nil { return } _, err = h.ei.InjectDynamic(h.packet.Auth.Password) if err != nil { return } h.containsDynamicField["basicauth"] = true } if h.envRgx.MatchString(h.packet.Auth.Username) || h.envRgx.MatchString(h.packet.Auth.Password) { h.containsEnvVar["basicauth"] = true } return } func (h *HttpRequester) Done() { // MaxIdleConnsPerHost and MaxIdleConns at Transport layer configuration // let us reuse the connections when keep-alive enabled(default) // When the Job is finished, we have to Close idle connections to prevent sockets to lock in at the TIME_WAIT state. // Otherwise, the next job can't use these sockets because they are reserved for the current target host. h.client.CloseIdleConnections() } func (h *HttpRequester) Send(client *http.Client, envs map[string]interface{}) (res *types.ScenarioStepResult) { var statusCode int var contentLength int64 var requestErr types.RequestError var reqStartTime = time.Now() // for debug mode var copiedReqBody []byte var respBody []byte var respHeaders http.Header var bodyReadErr error var extractedVars = make(map[string]interface{}) var failedCaptures = make(map[string]string, 0) var failedAssertions = make([]types.FailedAssertion, 0) var usableVars = make(map[string]interface{}, len(envs)) for k, v := range envs { usableVars[k] = v } if client == nil { // engine mode is 'ddosify' // if passed client is nil , use requesters client that is dedicated to one step, thereby one transport client = h.client } else { // engine mode is 'distinct-user' or 'repeated-user' // passed client is used for multiple steps throughout an iteration, update transport if client.Transport == nil { client.Transport = h.initTransport() client.Transport.(*http.Transport).MaxConnsPerHost = 1 // use same connection per host throughout an iteration } else { h.updateTransport(client.Transport.(*http.Transport)) } // update client timeout client.Timeout = time.Duration(h.packet.Timeout) * time.Second } durations := &duration{ serverProcessDurCh: make(chan time.Duration, 1), serverProcessStartCh: make(chan time.Time, 1), resDurCh: make(chan time.Duration, 1), resStartCh: make(chan time.Time, 1), } headersAddedByClient := make(map[string][]string) trace := newTrace(durations, h.proxyAddr, headersAddedByClient) httpReq, err := h.prepareReq(usableVars, trace) if err != nil { // could not prepare req requestErr.Type = types.ErrorInvalidRequest requestErr.Reason = fmt.Sprintf("Could not prepare req, %s", err.Error()) res = &types.ScenarioStepResult{ StepID: h.packet.ID, StepName: h.packet.Name, RequestID: uuid.New(), Err: requestErr, } return res } if httpReq.Body != nil { if int64(len(h.packet.Payload)) > 300000 { // Don't store req bodies bigger than 300KB copiedReqBody = []byte("too long body") } else { // TODO: make copiedReqBody an io.Reader, and pass same underlying buffer to both httpReq and copiedReqBody // copy buf := new(bytes.Buffer) io.Copy(buf, httpReq.Body) copiedReqBody = buf.Bytes() httpReq.Body = io.NopCloser(bytes.NewBuffer(copiedReqBody)) // restore body } } // Action httpRes, err := client.Do(httpReq) if err != nil { requestErr = fetchErrType(err) failedCaptures = h.captureEnvironmentVariables(nil, nil, nil, extractedVars) } else { // got response, no timeout or any other error, resStart should be set durations.setResDur() } // From the DOC: If the Body is not both read to EOF and closed, // the Client's underlying RoundTripper (typically Transport) // may not be able to re-use a persistent TCP connection to the server for a subsequent "keep-alive" request. if httpRes != nil { // read resp body conditionally if h.debug || len(h.packet.EnvsToCapture) > 0 || len(h.packet.Assertions) > 0 { respBody, bodyReadErr = io.ReadAll(httpRes.Body) if bodyReadErr != nil { requestErr = fetchErrType(bodyReadErr) } } else { // do not write into memory, just read _, bodyReadErr = io.Copy(io.Discard, httpRes.Body) if bodyReadErr != nil { requestErr = fetchErrType(bodyReadErr) } } httpRes.Body.Close() respHeaders = httpRes.Header contentLength = httpRes.ContentLength statusCode = httpRes.StatusCode cookies := make(map[string]*http.Cookie, len(httpRes.Cookies())) for _, cookie := range httpRes.Cookies() { cookies[cookie.Name] = &http.Cookie{ Name: cookie.Name, Value: cookie.Value, Path: cookie.Path, Domain: cookie.Domain, Expires: cookie.Expires, Secure: cookie.Secure, HttpOnly: cookie.HttpOnly, SameSite: cookie.SameSite, Raw: cookie.Raw, Unparsed: cookie.Unparsed, } } // capture if len(h.packet.EnvsToCapture) > 0 { failedCaptures = h.captureEnvironmentVariables(httpRes.Header, respBody, cookies, extractedVars) } // assert if len(h.packet.Assertions) > 0 { _, failedAssertions = h.applyAssertions(&evaluator.AssertEnv{ StatusCode: int64(httpRes.StatusCode), ResponseSize: int64(len(respBody)), ResponseTime: durations.totalDuration().Milliseconds(), // in ms Body: string(respBody), Headers: httpRes.Header, Variables: concatEnvs(envs, extractedVars), Cookies: cookies, }) } } var ddResTime time.Duration if httpRes != nil && httpRes.Header.Get("x-ddsfy-response-time") != "" { resTime, _ := strconv.ParseFloat(httpRes.Header.Get("x-ddsfy-response-time"), 8) ddResTime = time.Duration(resTime*1000) * time.Millisecond } // close duration channels, so that if any goroutine is waiting on them, it can return go time.AfterFunc(10*time.Millisecond, durationCloseFunc(durations)) // Finalize res = &types.ScenarioStepResult{ StepID: h.packet.ID, StepName: h.packet.Name, RequestID: uuid.New(), StatusCode: statusCode, RequestTime: reqStartTime, Duration: durations.totalDuration(), ContentLength: contentLength, Err: requestErr, Url: httpReq.URL.String(), Method: httpReq.Method, ReqHeaders: concatHeaders(httpReq.Header, headersAddedByClient), ReqBody: copiedReqBody, RespHeaders: respHeaders, RespBody: respBody, Custom: map[string]interface{}{ "dnsDuration": durations.getDNSDur(), "connDuration": durations.getConnDur(), "reqDuration": durations.getReqDur(), "resDuration": durations.getResDur(), "serverProcessDuration": durations.getServerProcessDur(), }, ExtractedEnvs: extractedVars, UsableEnvs: usableVars, FailedCaptures: failedCaptures, FailedAssertions: failedAssertions, } if strings.EqualFold(h.request.URL.Scheme, types.ProtocolHTTPS) { // TODOcorr : check here, used URL.scheme instead TODOcorr res.Custom["tlsDuration"] = durations.getTLSDur() } if ddResTime != 0 { res.Custom["ddResponseTime"] = ddResTime } return } var durationCloseFunc = func(d *duration) func() { return func() { d.close() } } func concatEnvs(envs1, envs2 map[string]interface{}) map[string]interface{} { total := make(map[string]interface{}) for k, v := range envs1 { total[k] = v } for k, v := range envs2 { total[k] = v } return total } func concatHeaders(envs1, envs2 map[string][]string) map[string][]string { total := make(map[string][]string) for k, v := range envs1 { total[k] = v } for k, v := range envs2 { total[k] = v } return total } func (h *HttpRequester) prepareReq(envs map[string]interface{}, trace *httptrace.ClientTrace) (*http.Request, error) { re := regexp.MustCompile(regex.DynamicVariableRegex) httpReq := h.request.Clone(h.ctx) body := h.packet.Payload if h.containsDynamicField["body"] || h.containsEnvVar["body"] { pieces := h.ei.GenerateBodyPieces(body, envs) customReader := injection.DdosifyBodyReader{ Body: body, Pieces: pieces, } httpReq.Body = &customReader httpReq.ContentLength = int64(injection.GetContentLength(pieces)) } else { // if body is constant, we can just set it httpReq.Body = io.NopCloser(bytes.NewReader(injection.StringToBytes(body))) httpReq.ContentLength = int64(len(body)) } // url hostURL := h.packet.URL var errURL error if h.containsDynamicField["url"] { hostURL, _ = h.ei.InjectDynamic(hostURL) } if h.containsEnvVar["url"] { hostURL, errURL = h.ei.InjectEnv(hostURL, envs) if errURL != nil { return nil, errURL } } httpReq.URL, errURL = url.Parse(hostURL) if errURL != nil { return nil, errURL } // If Host is not given in the header, set it from the original URL // Note that a temporary url used in initRequest if httpReq.Header.Get("Host") == "" { httpReq.Host = httpReq.URL.Host } // header if h.containsDynamicField["header"] { for k, values := range httpReq.Header { for _, v := range values { kk := k vv := v if re.MatchString(v) { vv, _ = h.ei.InjectDynamic(v) } if re.MatchString(k) { kk, _ = h.ei.InjectDynamic(k) httpReq.Header.Del(k) } httpReq.Header.Set(kk, vv) } } } if h.containsEnvVar["header"] { for k, v := range httpReq.Header { // check vals for i, vv := range v { if h.envRgx.MatchString(vv) { vvv, err := h.ei.InjectEnv(vv, envs) if err != nil { return nil, err } v[i] = vvv } } httpReq.Header.Set(k, strings.Join(v, ",")) // check keys if h.envRgx.MatchString(k) { kk, err := h.ei.InjectEnv(k, envs) if err != nil { return nil, err } httpReq.Header.Del(k) httpReq.Header.Set(kk, strings.Join(v, ",")) } } } username, password := h.packet.Auth.Username, h.packet.Auth.Password if h.containsDynamicField["basicauth"] { username, _ = h.ei.InjectDynamic(username) password, _ = h.ei.InjectDynamic(password) } if h.containsEnvVar["basicauth"] { var err error username, err = h.ei.InjectEnv(username, envs) if err != nil { return nil, err } password, err = h.ei.InjectEnv(password, envs) if err != nil { return nil, err } } if username != "" && password != "" { httpReq.SetBasicAuth(username, password) } httpReq = httpReq.WithContext(httptrace.WithClientTrace(httpReq.Context(), trace)) return httpReq, nil } // Currently we can't detect exact error type by returned err. // But we need to find an elegant way instead of this. func fetchErrType(err error) types.RequestError { var requestErr types.RequestError = types.RequestError{ Type: types.ErrorUnkown, Reason: err.Error()} ue, ok := err.(*url.Error) if ok { errString := ue.Error() if strings.Contains(errString, "proxyconnect") { if strings.Contains(errString, "connection refused") { requestErr = types.RequestError{Type: types.ErrorProxy, Reason: types.ReasonProxyFailed} } else if strings.Contains(errString, "Client.Timeout") { requestErr = types.RequestError{Type: types.ErrorProxy, Reason: types.ReasonProxyTimeout} } else { requestErr = types.RequestError{Type: types.ErrorProxy, Reason: errString} } } else if strings.Contains(errString, context.DeadlineExceeded.Error()) { requestErr = types.RequestError{Type: types.ErrorConn, Reason: types.ReasonConnTimeout} } else if strings.Contains(errString, "Client.Timeout exceeded while awaiting headers") { requestErr = types.RequestError{Type: types.ErrorConn, Reason: types.ReasonConnTimeout} } else if strings.Contains(errString, "i/o timeout") { requestErr = types.RequestError{Type: types.ErrorConn, Reason: types.ReasonReadTimeout} } else if strings.Contains(errString, "connection refused") { requestErr = types.RequestError{Type: types.ErrorConn, Reason: types.ReasonConnRefused} } else if strings.Contains(errString, context.Canceled.Error()) { requestErr = types.RequestError{Type: types.ErrorIntented, Reason: types.ReasonCtxCanceled} } else if strings.Contains(errString, "connection reset by peer") { requestErr = types.RequestError{Type: types.ErrorConn, Reason: "connection reset by peer"} } else { requestErr = types.RequestError{Type: types.ErrorConn, Reason: errString} } } return requestErr } func (h *HttpRequester) initTransport() *http.Transport { tr := &http.Transport{ TLSClientConfig: h.initTLSConfig(), Proxy: http.ProxyURL(h.proxyAddr), } tr.DisableKeepAlives = false if h.packet.Headers["Connection"] == "close" { tr.DisableKeepAlives = true } if val, ok := h.packet.Custom["disable-compression"]; ok { tr.DisableCompression = val.(bool) } if val, ok := h.packet.Custom["h2"]; ok { val := val.(bool) if val { http2.ConfigureTransport(tr) } } return tr } func (h *HttpRequester) updateTransport(tr *http.Transport) { tr.TLSClientConfig = h.initTLSConfig() tr.Proxy = http.ProxyURL(h.proxyAddr) tr.DisableKeepAlives = false if h.packet.Headers["Connection"] == "close" { tr.DisableKeepAlives = true } if val, ok := h.packet.Custom["disable-compression"]; ok { tr.DisableCompression = val.(bool) } if val, ok := h.packet.Custom["h2"]; ok { val := val.(bool) if val { http2.ConfigureTransport(tr) } } } func (h *HttpRequester) initTLSConfig() *tls.Config { tlsConfig := &tls.Config{ InsecureSkipVerify: true, } if h.packet.CertPool != nil && h.packet.Cert.Certificate != nil { tlsConfig.RootCAs = h.packet.CertPool tlsConfig.Certificates = []tls.Certificate{h.packet.Cert} } if val, ok := h.packet.Custom["hostname"]; ok { tlsConfig.ServerName = val.(string) } return tlsConfig } func (h *HttpRequester) initRequestInstance() (err error) { // TODOcorr: https://{{TARGET_URL}} or http://{{TARGET_URL}} could not be parsed, invalidHost // give a basic url for now here to avoid initiating request every time // override later on prepareReq tempValidUrl := "app.ddosify.com" if strings.HasPrefix(h.packet.URL, "https://") { tempValidUrl = "https://" + "app.ddosify.com" } h.request, err = http.NewRequest(h.packet.Method, tempValidUrl, bytes.NewBufferString(h.packet.Payload)) if err != nil { return } // Headers header := make(http.Header) for k, v := range h.packet.Headers { header.Set(k, v) // Since we use a temp url, we need to override the request.Host either // it will be app.ddosify.com // or it will be the host from the headers // later on prepareReq, we will override the host if it is set in the headers if strings.EqualFold(k, "Host") { h.request.Host = v } } h.request.Header = header // Auth should be set after header assignment. if h.packet.Auth != (types.Auth{}) { h.request.SetBasicAuth(h.packet.Auth.Username, h.packet.Auth.Password) } // If keep-alive is false, prevent the reuse of the previous TCP connection at the request layer also. h.request.Close = false if h.packet.Headers["Connection"] == "close" { h.request.Close = true } return } func (h *HttpRequester) Type() string { return "HTTP" } func newTrace(duration *duration, proxyAddr *url.URL, headersByClient map[string][]string) *httptrace.ClientTrace { var dnsStart, connStart, tlsStart, reqStart time.Time // According to the doc in the trace.go; // Some of the hooks below can be triggered multiple times in case of retried connections, "Happy Eyeballs" etc.. // Also, some of the hooks can be triggered after the TCP roundtrip if the request is not successfully finished. // To fetch the time only at the first trigger and prevent data race we need to use the mutex mechanism. // For start times, except resStart, this mutex is been using. // For duration calculations, "duration" struct internally uses another mutex. var m sync.Mutex return &httptrace.ClientTrace{ DNSStart: func(info httptrace.DNSStartInfo) { m.Lock() if dnsStart.IsZero() { dnsStart = time.Now() } m.Unlock() }, DNSDone: func(dnsInfo httptrace.DNSDoneInfo) { m.Lock() // no need to handle error in here. We can detect it at http.Client.Do return. if dnsInfo.Err == nil { duration.setDNSDur(time.Since(dnsStart)) } m.Unlock() }, ConnectStart: func(network, addr string) { m.Lock() if connStart.IsZero() { connStart = time.Now() } m.Unlock() }, ConnectDone: func(network, addr string, err error) { m.Lock() // no need to handle error in here. We can detect it at http.Client.Do return. if err == nil { duration.setConnDur(time.Since(connStart)) } m.Unlock() }, TLSHandshakeStart: func() { m.Lock() // This hook can be hit 2 times; // If both proxy and target are HTTPS // First hit is for proxy, second is for target. // To catch the second TLS start time (for target), we can't perform tlsStart.IsZero() check here. tlsStart = time.Now() m.Unlock() }, TLSHandshakeDone: func(cs tls.ConnectionState, e error) { m.Lock() // This hook can be hit 2 times; // If proxy: HTTPS, target: HTTPS // First hit is for proxy, second is for target TLS // We need to calculate TLS duration if and only if the TLS handshake process is for the target. if e == nil { if proxyAddr == nil || proxyAddr.Hostname() != cs.ServerName { duration.setTLSDur(time.Since(tlsStart)) } } m.Unlock() }, GotConn: func(connInfo httptrace.GotConnInfo) { m.Lock() if reqStart.IsZero() { reqStart = time.Now() } m.Unlock() }, WroteRequest: func(w httptrace.WroteRequestInfo) { // no need to handle error in here. We can detect it at http.Client.Do return. if w.Err == nil { go duration.setReqDur(time.Since(reqStart)) go duration.setServerProcessStart(time.Now()) } }, GotFirstResponseByte: func() { go duration.setServerProcessDur() go duration.setResStartTime(time.Now()) }, WroteHeaderField: func(key string, value []string) { headersByClient[key] = value }, } } func (h *HttpRequester) applyAssertions(assertEnv *evaluator.AssertEnv) (bool, []types.FailedAssertion) { // result, failedAssertionIndex, assertionError assertions := h.packet.Assertions assertionsSuccess := true failedAssertions := []types.FailedAssertion{} for _, rule := range assertions { boolVal, err := assertion.Assert(rule, assertEnv) if err != nil { assertErr := err.(assertion.AssertionError) failedAssertions = append(failedAssertions, types.FailedAssertion{ Rule: assertErr.Rule(), Received: assertErr.Received(), Reason: assertErr.Unwrap().Error(), }) assertionsSuccess = false } if !boolVal { assertionsSuccess = false } } if assertionsSuccess { return true, nil } return false, failedAssertions } func (h *HttpRequester) captureEnvironmentVariables(header http.Header, respBody []byte, cookies map[string]*http.Cookie, extractedVars map[string]interface{}) map[string]string { var err error failedCaptures := make(map[string]string, 0) var captureError extraction.ExtractionError // request failed, only set default value for later steps if header == nil && respBody == nil { for _, ce := range h.packet.EnvsToCapture { extractedVars[ce.Name] = "" // default value for not extracted envs failedCaptures[ce.Name] = "request failed" } return failedCaptures } // extract from response for _, ce := range h.packet.EnvsToCapture { var val interface{} switch ce.From { case types.Header: val, err = extraction.Extract(header, ce) case types.Body: val, err = extraction.Extract(respBody, ce) case types.Cookie: val, err = extraction.Extract(cookies, ce) } if err != nil && errors.As(err, &captureError) { // do not terminate in case of a capture error, continue capturing extractedVars[ce.Name] = "" // default value for not extracted envs failedCaptures[ce.Name] = captureError.Error() continue } extractedVars[ce.Name] = val } return failedCaptures } type duration struct { // DNS lookup duration. If IP:Port porvided instead of domain, this will be 0 dnsDur time.Duration // TCP connection setup duration connDur time.Duration // TLS handshake duration. For HTTP this will be 0 tlsDur time.Duration // Request write duration reqDur time.Duration // Response read duration resDur time.Duration // Duration between full request write to first response. AKA Time To First Byte (TTFB) serverProcessDur time.Duration // Time at response reading start resStart time.Time resStartCh chan time.Time resStartChClosed bool serverProcessDurCh chan time.Duration serverProcessDurChClosed bool serverProcessStartCh chan time.Time serverProcessStartChClosed bool resDurCh chan time.Duration resDurChClosed bool mu sync.Mutex chMu sync.Mutex getChLock sync.Mutex } func (d *duration) setResStartTime(t time.Time) { d.chMu.Lock() defer d.chMu.Unlock() if !d.resStartChClosed { d.resStartCh <- t d.resStartChClosed = true close(d.resStartCh) } } // this maybe called multiple times in case of retried requests by WroteRequest hook func (d *duration) setServerProcessStart(t time.Time) { d.chMu.Lock() defer d.chMu.Unlock() if !d.serverProcessStartChClosed { d.serverProcessStartCh <- t d.serverProcessStartChClosed = true close(d.serverProcessStartCh) } } func (d *duration) setDNSDur(t time.Duration) { d.mu.Lock() defer d.mu.Unlock() if d.dnsDur == 0 { d.dnsDur = t } } func (d *duration) getDNSDur() time.Duration { d.mu.Lock() defer d.mu.Unlock() return d.dnsDur } func (d *duration) setTLSDur(t time.Duration) { d.mu.Lock() defer d.mu.Unlock() if d.tlsDur == 0 { d.tlsDur = t } } func (d *duration) getTLSDur() time.Duration { d.mu.Lock() defer d.mu.Unlock() return d.tlsDur } func (d *duration) setConnDur(t time.Duration) { d.mu.Lock() defer d.mu.Unlock() if d.connDur == 0 { d.connDur = t } } func (d *duration) getConnDur() time.Duration { d.mu.Lock() defer d.mu.Unlock() return d.connDur } func (d *duration) setReqDur(t time.Duration) { d.mu.Lock() defer d.mu.Unlock() if d.reqDur == 0 { d.reqDur = t } } func (d *duration) getReqDur() time.Duration { d.mu.Lock() defer d.mu.Unlock() return d.reqDur } func (d *duration) setServerProcessDur() { serverProcessStart := <-d.serverProcessStartCh d.chMu.Lock() defer d.chMu.Unlock() if !d.serverProcessDurChClosed { d.serverProcessDurCh <- time.Since(serverProcessStart) d.serverProcessDurChClosed = true close(d.serverProcessDurCh) } } func (d *duration) getServerProcessDur() time.Duration { d.getChLock.Lock() defer d.getChLock.Unlock() serverProcessDur, ok := <-d.serverProcessDurCh if !ok { // channel closed, dur already set or closed by timer return d.serverProcessDur } d.serverProcessDur = serverProcessDur return d.serverProcessDur } func (d *duration) setResDur() { resStart := <-d.resStartCh d.chMu.Lock() defer d.chMu.Unlock() if !d.resDurChClosed { d.resDurCh <- time.Since(resStart) d.resDurChClosed = true close(d.resDurCh) } } func (d *duration) getResDur() time.Duration { d.getChLock.Lock() defer d.getChLock.Unlock() resDur, ok := <-d.resDurCh if !ok { // channel closed, probably resDur already set and chan closed by sender return d.resDur } d.resDur = resDur return d.resDur } func (d *duration) totalDuration() time.Duration { d.mu.Lock() defer d.mu.Unlock() return d.dnsDur + d.connDur + d.tlsDur + d.reqDur + d.getServerProcessDur() + d.getResDur() } // normally channels are closed by sender, but in case of senders are not called, we close them here func (d *duration) close() { d.chMu.Lock() defer d.chMu.Unlock() // close channels func() { defer func() { if r := recover(); r != nil { // channel already closed } }() d.serverProcessStartChClosed = true close(d.serverProcessStartCh) }() func() { defer func() { if r := recover(); r != nil { // channel already closed } }() d.serverProcessDurChClosed = true close(d.serverProcessDurCh) }() func() { defer func() { if r := recover(); r != nil { // channel already closed } }() d.resStartChClosed = true close(d.resStartCh) }() func() { defer func() { if r := recover(); r != nil { // channel already closed } }() d.resDurChClosed = true close(d.resDurCh) }() } ================================================ FILE: ddosify_engine/core/scenario/requester/http_test.go ================================================ /* * * Ddosify - Load testing tool for any web system. * Copyright (C) 2021 Ddosify (https://ddosify.com) * * This program is free software: you can redistribute it and/or modify * it under the terms of the GNU Affero General Public License as published * by the Free Software Foundation, either version 3 of the License, or * (at your option) any later version. * * This program is distributed in the hope that it will be useful, * but WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * GNU Affero General Public License for more details. * * You should have received a copy of the GNU Affero General Public License * along with this program. If not, see . * */ package requester import ( "bytes" "context" "crypto/tls" "net/http" "net/http/httptest" "net/http/httptrace" "net/url" "reflect" "strings" "testing" "time" "go.ddosify.com/ddosify/core/types" "golang.org/x/net/http2" ) func TestInit(t *testing.T) { s := types.ScenarioStep{ ID: 1, Method: http.MethodGet, URL: "https://test.com", Timeout: types.DefaultTimeout, } p, _ := url.Parse("https://127.0.0.1:80") ctx := context.TODO() h := &HttpRequester{} h.Init(ctx, s, p, false, nil) if !reflect.DeepEqual(h.packet, s) { t.Errorf("Expected %v, Found %v", s, h.packet) } if !reflect.DeepEqual(h.proxyAddr, p) { t.Errorf("Expected %v, Found %v", p, h.proxyAddr) } if !reflect.DeepEqual(h.ctx, ctx) { t.Errorf("Expected %v, Found %v", ctx, h.ctx) } } func TestInitClient(t *testing.T) { p, _ := url.Parse("https://127.0.0.1:80") ctx := context.TODO() // Basic Client s := types.ScenarioStep{ ID: 1, Method: http.MethodGet, URL: "https://test.com", Timeout: types.DefaultTimeout, } expectedTLS := &tls.Config{ InsecureSkipVerify: true, } expectedTr := &http.Transport{ TLSClientConfig: expectedTLS, Proxy: http.ProxyURL(p), DisableKeepAlives: false, } expectedClient := &http.Client{ Transport: expectedTr, Timeout: time.Duration(types.DefaultTimeout) * time.Second, } // Client with custom data sWithCustomData := types.ScenarioStep{ ID: 1, Method: http.MethodGet, URL: "https://test.com", Timeout: types.DefaultTimeout, Headers: map[string]string{"Connection": "close"}, Custom: map[string]interface{}{ "disable-redirect": true, "disable-compression": true, "hostname": "dummy.com", }, } expectedTLSCustomData := &tls.Config{ InsecureSkipVerify: true, ServerName: "dummy.com", } expectedTrCustomData := &http.Transport{ TLSClientConfig: expectedTLSCustomData, Proxy: http.ProxyURL(p), DisableKeepAlives: true, DisableCompression: true, } expectedClientWithCustomData := &http.Client{ Transport: expectedTrCustomData, CheckRedirect: func(req *http.Request, via []*http.Request) error { return http.ErrUseLastResponse }, Timeout: time.Duration(types.DefaultTimeout) * time.Second, } // H2 Client sHTTP2 := types.ScenarioStep{ ID: 1, Method: http.MethodGet, URL: "https://test.com", Timeout: types.DefaultTimeout, Custom: map[string]interface{}{ "h2": true, }, } expectedTLSHTTP2 := &tls.Config{ InsecureSkipVerify: true, } expectedTrHTTP2 := &http.Transport{ TLSClientConfig: expectedTLSHTTP2, Proxy: http.ProxyURL(p), DisableKeepAlives: false, } http2.ConfigureTransport(expectedTrHTTP2) expectedClientHTTP2 := &http.Client{ Transport: expectedTrHTTP2, Timeout: time.Duration(types.DefaultTimeout) * time.Second, } // Sub Tests tests := []struct { name string scenarioItem types.ScenarioStep proxy *url.URL ctx context.Context tls *tls.Config transport *http.Transport client *http.Client }{ {"Basic", s, p, ctx, expectedTLS, expectedTr, expectedClient}, {"Custom", sWithCustomData, p, ctx, expectedTLSCustomData, expectedTrCustomData, expectedClientWithCustomData}, {"HTTP2", sHTTP2, p, ctx, expectedTLSHTTP2, expectedTrHTTP2, expectedClientHTTP2}, } for _, test := range tests { tf := func(t *testing.T) { h := &HttpRequester{} h.Init(test.ctx, test.scenarioItem, test.proxy, false, nil) transport := h.client.Transport.(*http.Transport) tls := transport.TLSClientConfig // TLS Assert (Also check HTTP2 vs HTTP) if !reflect.DeepEqual(test.tls, tls) { t.Errorf("\nTLS Expected %#v, \nFound %#v", test.tls, tls) } // Transport Assert if reflect.TypeOf(test.transport) != reflect.TypeOf(transport) { // Compare HTTP2 configured transport vs HTTP transport t.Errorf("Transport Type Expected %#v, Found %#v", test.transport, transport) } pFunc := transport.Proxy == nil expectedPFunc := test.transport.Proxy == nil if pFunc != expectedPFunc { t.Errorf("Proxy Expected %v, Found %v", expectedPFunc, pFunc) } if test.transport.DisableKeepAlives != transport.DisableKeepAlives { t.Errorf("DisableKeepAlives Expected %v, Found %v", test.transport.DisableKeepAlives, transport.DisableKeepAlives) } if test.transport.DisableCompression != transport.DisableCompression { t.Errorf("DisableCompression Expected %v, Found %v", test.transport.DisableCompression, transport.DisableCompression) } // Client Assert if test.client.Timeout != h.client.Timeout { t.Errorf("Timeout Expected %v, Found %v", test.client.Timeout, h.client.Timeout) } crFunc := h.client.CheckRedirect == nil expectedCRFunc := test.client.CheckRedirect == nil if expectedCRFunc != crFunc { t.Errorf("CheckRedirect Expected %v, Found %v", expectedCRFunc, crFunc) } } t.Run(test.name, tf) } } func TestInitRequest(t *testing.T) { p, _ := url.Parse("https://127.0.0.1:80") ctx := context.TODO() // Invalid request sInvalid := types.ScenarioStep{ ID: 1, Method: ":31:31:#", URL: "https://test.com", Payload: "payloadtest", } // Basic request s := types.ScenarioStep{ ID: 1, Method: http.MethodGet, URL: "https://test.com", Payload: "payloadtest", } expected, _ := http.NewRequest(s.Method, s.URL, bytes.NewBufferString(s.Payload)) expected.Close = false expected.Header = make(http.Header) // Request with auth sWithAuth := types.ScenarioStep{ ID: 1, Method: http.MethodGet, URL: "https://test.com", Payload: "payloadtest", Auth: types.Auth{ Username: "test", Password: "123", }, } expectedWithAuth, _ := http.NewRequest(sWithAuth.Method, sWithAuth.URL, bytes.NewBufferString(sWithAuth.Payload)) expectedWithAuth.Close = false expectedWithAuth.Header = make(http.Header) expectedWithAuth.SetBasicAuth(sWithAuth.Auth.Username, sWithAuth.Auth.Password) // Request With Headers sWithHeaders := types.ScenarioStep{ ID: 1, Method: http.MethodGet, URL: "https://test.localhost", Payload: "payloadtest", Auth: types.Auth{ Username: "test", Password: "123", }, Headers: map[string]string{ "Header1": "Value1", "Header2": "Value2", "User-Agent": "Firefox", "Host": "test.com", }, } expectedWithHeaders, _ := http.NewRequest(sWithHeaders.Method, sWithHeaders.URL, bytes.NewBufferString(sWithHeaders.Payload)) expectedWithHeaders.Close = false expectedWithHeaders.Header = make(http.Header) expectedWithHeaders.Header.Set("Header1", "Value1") expectedWithHeaders.Header.Set("Header2", "Value2") expectedWithHeaders.Header.Set("User-Agent", "Firefox") expectedWithHeaders.Header.Set("Host", "test.com") expectedWithHeaders.Host = "test.com" expectedWithHeaders.SetBasicAuth(sWithHeaders.Auth.Username, sWithHeaders.Auth.Password) // Request keep-alive condition sWithoutKeepAlive := types.ScenarioStep{ ID: 1, Method: http.MethodGet, URL: "https://test.com", Payload: "payloadtest", Auth: types.Auth{ Username: "test", Password: "123", }, Headers: map[string]string{ "Header1": "Value1", "Header2": "Value2", "Connection": "close", }, } expectedWithoutKeepAlive, _ := http.NewRequest(sWithoutKeepAlive.Method, sWithoutKeepAlive.URL, bytes.NewBufferString(sWithoutKeepAlive.Payload)) expectedWithoutKeepAlive.Close = true expectedWithoutKeepAlive.Header = make(http.Header) expectedWithoutKeepAlive.Header.Set("Header1", "Value1") expectedWithoutKeepAlive.Header.Set("Header2", "Value2") expectedWithoutKeepAlive.Header.Set("Connection", "close") expectedWithoutKeepAlive.SetBasicAuth(sWithoutKeepAlive.Auth.Username, sWithoutKeepAlive.Auth.Password) // Sub Tests tests := []struct { name string scenarioItem types.ScenarioStep shouldErr bool request *http.Request }{ {"Invalid", sInvalid, true, nil}, {"Basic", s, false, expected}, {"WithAuth", sWithAuth, false, expectedWithAuth}, {"WithHeaders", sWithHeaders, false, expectedWithHeaders}, {"WithoutKeepAlive", sWithoutKeepAlive, false, expectedWithoutKeepAlive}, } for _, test := range tests { tf := func(t *testing.T) { h := &HttpRequester{} err := h.Init(ctx, test.scenarioItem, p, false, nil) if test.shouldErr { if err == nil { t.Errorf("Should be errored") } } else { if err != nil { t.Errorf("Errored: %v", err) } // TODOcorr: we use tempValidUrl for correlation for now // if !reflect.DeepEqual(h.request.URL, test.request.URL) { // t.Errorf("URL Expected: %#v, Found: \n%#v", test.request.URL, h.request.URL) // } // if !reflect.DeepEqual(h.request.Host, test.request.Host) { // t.Errorf("Host Expected: %#v, Found: \n%#v", test.request.Host, h.request.Host) // } if !reflect.DeepEqual(h.request.Body, test.request.Body) { t.Errorf("Body Expected: %#v, Found: \n%#v", test.request.Body, h.request.Body) } if !reflect.DeepEqual(h.request.Header, test.request.Header) { t.Errorf("Header Expected: %#v, Found: \n%#v", test.request.Header, h.request.Header) } if !reflect.DeepEqual(h.request.Close, test.request.Close) { t.Errorf("Close Expected: %#v, Found: \n%#v", test.request.Close, h.request.Close) } } } t.Run(test.name, tf) } } func TestSendOnDebugModePopulatesDebugInfo(t *testing.T) { ctx := context.TODO() // Basic request payload := "reqbodypayload" s := types.ScenarioStep{ ID: 1, Method: http.MethodGet, URL: "https://ddosify.com", Payload: payload, Headers: map[string]string{"X": "y"}, } expectedUrl := "https://ddosify.com" expectedMethod := http.MethodGet expectedRequestHeaders := http.Header{"X": {"y"}} expectedRequestBody := []byte(payload) tf := func(t *testing.T) { h := &HttpRequester{} debug := true var proxy *url.URL _ = h.Init(ctx, s, proxy, debug, nil) envs := map[string]interface{}{} res := h.Send(http.DefaultClient, envs) if expectedMethod != res.Method { t.Errorf("Method Expected %#v, Found: \n%#v", expectedMethod, res.Method) } if expectedUrl != res.Url { t.Errorf("Url Expected %#v, Found: \n%#v", expectedUrl, res.Url) } if !bytes.Equal(expectedRequestBody, res.ReqBody) { t.Errorf("RequestBody Expected %#v, Found: \n%#v", expectedRequestBody, res.ReqBody) } // stepResult has default request headers added by go client for expKey, expVal := range expectedRequestHeaders { if !reflect.DeepEqual(expVal, res.ReqHeaders.Values(expKey)) { t.Errorf("RequestHeaders Expected %#v, Found: \n%#v", expectedRequestHeaders, res.ReqHeaders) } } } t.Run("populate-debug-info", tf) } func TestCaptureEnvShouldSetEmptyStringWhenReqFails(t *testing.T) { ctx := context.TODO() // Failed request envName := "ENV_NAME" headerKey := "key" s := types.ScenarioStep{ ID: 1, Method: http.MethodGet, URL: "https://ddosifyInvalid.com", EnvsToCapture: []types.EnvCaptureConf{{ JsonPath: new(string), Xpath: new(string), RegExp: &types.RegexCaptureConf{}, Name: envName, From: types.Header, Key: &headerKey, }}, } expectedExtractedEnvs := map[string]interface{}{ envName: "", } // Sub Tests tests := []struct { name string scenarioStep types.ScenarioStep expectedExtractedEnvs map[string]interface{} }{ {"ExtractedEnvShouldBeEmptyStringWhenReqFailure", s, expectedExtractedEnvs}, } for _, test := range tests { tf := func(t *testing.T) { h := &HttpRequester{} debug := true var proxy *url.URL _ = h.Init(ctx, test.scenarioStep, proxy, debug, nil) envs := map[string]interface{}{} tempDurationClose := durationCloseFunc durationCloseCalled := false durationCloseFunc = func(d *duration) func() { return func() { tempDurationClose(d)() durationCloseCalled = true } } defer func() { durationCloseFunc = tempDurationClose }() res := h.Send(http.DefaultClient, envs) if !durationCloseCalled { t.Errorf("Duration close should be called") } if !reflect.DeepEqual(res.ExtractedEnvs, test.expectedExtractedEnvs) { t.Errorf("Extracted env should be set empty string on req failure") } } t.Run(test.name, tf) } } func TestAssertions(t *testing.T) { t.Parallel() // Test server firstReqHandler := func(w http.ResponseWriter, r *http.Request) { w.Header().Set("Argentina", "Messi") w.WriteHeader(http.StatusForbidden) } rule1 := "equals(status_code,405)" rule2 := `equals(headers.Argentina,"Ronaldo")` pathFirst := "/json-body" mux := http.NewServeMux() mux.HandleFunc(pathFirst, firstReqHandler) server := httptest.NewServer(mux) defer server.Close() s := types.ScenarioStep{ ID: 1, Method: "GET", URL: server.URL + pathFirst, Assertions: []string{rule1, rule2}, } ctx := context.TODO() h := &HttpRequester{} h.Init(ctx, s, nil, false, nil) res := h.Send(http.DefaultClient, map[string]interface{}{}) if !strings.EqualFold(res.FailedAssertions[0].Rule, rule1) { t.Errorf("rule expected %s, got %s", rule1, res.FailedAssertions[0].Rule) } if reflect.DeepEqual(res.FailedAssertions[0].Received, 403) { t.Errorf("received expected %d, got %v", 403, res.FailedAssertions[0].Received) } if !strings.EqualFold(res.FailedAssertions[1].Rule, rule2) { t.Errorf("rule expected %s, got %s", rule1, res.FailedAssertions[1].Rule) } if reflect.DeepEqual(res.FailedAssertions[0].Received, "Ronaldo") { t.Errorf("received expected %s, got %v", "Ronaldo", res.FailedAssertions[1].Received) } } func TestTraceResDur_TypicalScenario(t *testing.T) { var maxDuration int64 = 1<<63 - 1 d := &duration{ serverProcessDurCh: make(chan time.Duration, 1), serverProcessStartCh: make(chan time.Time, 1), resDurCh: make(chan time.Duration, 1), resStartCh: make(chan time.Time, 1), } trace := newTrace(d, nil, nil) // below two is called by different goroutines // typically wroteRequest is called before gotFirstResponseByte go func() { go trace.WroteRequest(httptrace.WroteRequestInfo{}) time.Sleep(10 * time.Millisecond) go trace.GotFirstResponseByte() }() // called by Send method d.setResDur() // called by Send method resDur := d.getResDur() if resDur == time.Duration(maxDuration) { t.Errorf("resDur should not be %d", maxDuration) } } func TestTraceResDur_UnusualScenario(t *testing.T) { var maxDuration int64 = 1<<63 - 1 d := &duration{ serverProcessDurCh: make(chan time.Duration, 1), serverProcessStartCh: make(chan time.Time, 1), resDurCh: make(chan time.Duration, 1), resStartCh: make(chan time.Time, 1), } trace := newTrace(d, nil, nil) // below two is called by different goroutines // typically wroteRequest is called before gotFirstResponseByte // we will simulate the opposite go func() { go trace.GotFirstResponseByte() time.Sleep(100 * time.Millisecond) go trace.WroteRequest(httptrace.WroteRequestInfo{}) }() // called by Send method d.setResDur() // called by Send method resDur := d.getResDur() if resDur == time.Duration(maxDuration) { t.Errorf("resDur should not be %d", maxDuration) } } func TestTraceServerProcessDur(t *testing.T) { var maxDuration int64 = 1<<63 - 1 d := &duration{ serverProcessDurCh: make(chan time.Duration, 1), serverProcessStartCh: make(chan time.Time, 1), resDurCh: make(chan time.Duration, 1), resStartCh: make(chan time.Time, 1), } trace := newTrace(d, nil, nil) // below two is called by different goroutines // typically wroteRequest is called before gotFirstResponseByte go func() { go trace.WroteRequest(httptrace.WroteRequestInfo{}) time.Sleep(10 * time.Millisecond) go trace.GotFirstResponseByte() }() // called by Send method // this get needs to wait for GotFirstResponseByte serverProcessDur := d.getServerProcessDur() if serverProcessDur == time.Duration(maxDuration) { t.Errorf("serverProcessDur should not be %d", maxDuration) } } func TestTraceServerProcessDur_2(t *testing.T) { var maxDuration int64 = 1<<63 - 1 d := &duration{ serverProcessDurCh: make(chan time.Duration, 1), serverProcessStartCh: make(chan time.Time, 1), resDurCh: make(chan time.Duration, 1), resStartCh: make(chan time.Time, 1), } trace := newTrace(d, nil, nil) // below two is called by different goroutines // typically wroteRequest is called before gotFirstResponseByte // we will simulate the opposite go func() { go trace.GotFirstResponseByte() time.Sleep(10 * time.Millisecond) go trace.WroteRequest(httptrace.WroteRequestInfo{}) }() // called by Send method // this get needs to wait for GotFirstResponseByte serverProcessDur := d.getServerProcessDur() if serverProcessDur == time.Duration(maxDuration) { t.Errorf("serverProcessDur should not be %d", maxDuration) } } func TestTraceServerProcessDur_3(t *testing.T) { var maxDuration int64 = 1<<63 - 1 d := &duration{ serverProcessDurCh: make(chan time.Duration, 1), serverProcessStartCh: make(chan time.Time, 1), resDurCh: make(chan time.Duration, 1), resStartCh: make(chan time.Time, 1), } trace := newTrace(d, nil, nil) // below two is called by different goroutines // typically wroteRequest is called before gotFirstResponseByte // we will simulate the opposite go func() { go trace.GotFirstResponseByte() time.Sleep(10 * time.Millisecond) go trace.WroteRequest(httptrace.WroteRequestInfo{}) }() // called by Send method // this get needs to wait for GotFirstResponseByte serverProcessDur1 := d.getServerProcessDur() if serverProcessDur1 == time.Duration(maxDuration) { t.Errorf("serverProcessDur should not be %d", maxDuration) } serverProcessDur2 := d.getServerProcessDur() if serverProcessDur1 != serverProcessDur2 { t.Errorf("serverProcessDur1 and serverProcessDur2 should be equal") } } func TestTraceServerProcessDur_ErrCase(t *testing.T) { var maxDuration int64 = 1<<63 - 1 d := &duration{ serverProcessDurCh: make(chan time.Duration, 1), serverProcessStartCh: make(chan time.Time, 1), resDurCh: make(chan time.Duration, 1), resStartCh: make(chan time.Time, 1), } trace := newTrace(d, nil, nil) // below two is called by different goroutines // typically wroteRequest is called before gotFirstResponseByte go func() { go trace.WroteRequest(httptrace.WroteRequestInfo{}) time.Sleep(10 * time.Millisecond) // not called in err case // go trace.GotFirstResponseByte() }() // called by Send method // channels should be closed, otherwise get calls can block forever durationCloseFunc(d)() // called by Send method // this get needs to wait for GotFirstResponseByte serverProcessDur := d.getServerProcessDur() if serverProcessDur == time.Duration(maxDuration) { t.Errorf("serverProcessDur should not be %d", maxDuration) } } func TestResponseCookiesSentToAssertions(t *testing.T) { t.Parallel() // Test server firstReqHandler := func(w http.ResponseWriter, r *http.Request) { http.SetCookie(w, &http.Cookie{Name: "Argentina", Value: "Messi"}) http.SetCookie(w, &http.Cookie{Name: "Goat", Value: "Messi"}) w.WriteHeader(http.StatusForbidden) } passRule := "equals(cookies.Argentina.value,\"Messi\")" failRule := "equals(cookies.Goat.value,\"Ronaldo\")" pathFirst := "/json-body" mux := http.NewServeMux() mux.HandleFunc(pathFirst, firstReqHandler) server := httptest.NewServer(mux) defer server.Close() s := types.ScenarioStep{ ID: 1, Method: "GET", URL: server.URL + pathFirst, Assertions: []string{passRule, failRule}, } ctx := context.TODO() h := &HttpRequester{} h.Init(ctx, s, nil, false, nil) res := h.Send(http.DefaultClient, map[string]interface{}{}) if len(res.FailedAssertions) != 1 { t.Errorf("expected 1 failed assertion, got %d", len(res.FailedAssertions)) } if !strings.EqualFold(res.FailedAssertions[0].Rule, failRule) { t.Errorf("rule expected %s, got %s", failRule, res.FailedAssertions[0].Rule) } if reflect.DeepEqual(res.FailedAssertions[0].Received, "Ronaldo") { t.Errorf("received expected %s, got %v", "Ronaldo", res.FailedAssertions[0].Received) } } ================================================ FILE: ddosify_engine/core/scenario/scripting/assertion/assert.go ================================================ package assertion import ( "fmt" "strings" "go.ddosify.com/ddosify/core/scenario/scripting/assertion/evaluator" "go.ddosify.com/ddosify/core/scenario/scripting/assertion/lexer" "go.ddosify.com/ddosify/core/scenario/scripting/assertion/parser" ) type AssertionError struct { // UnWrappable failedAssertion string received map[string]interface{} wrappedErr error } func (ae AssertionError) Error() string { return fmt.Sprintf("input : %s, received: %v, wrappedErr: %v", ae.failedAssertion, ae.received, ae.wrappedErr) } func (ae AssertionError) Rule() string { return ae.failedAssertion } func (ae AssertionError) Received() map[string]interface{} { return ae.received } func (ae AssertionError) Unwrap() error { return ae.wrappedErr } func Assert(input string, env *evaluator.AssertEnv) (bool, error) { l := lexer.New(input) p := parser.New(l) node := p.ParseExpressionStatement() if len(p.Errors()) > 0 { return false, AssertionError{ failedAssertion: input, received: map[string]interface{}{}, wrappedErr: fmt.Errorf(strings.Join(p.Errors(), ",")), } } receivedMap := make(map[string]interface{}) obj, err := evaluator.Eval(node, env, receivedMap) if err != nil { return false, AssertionError{ failedAssertion: input, received: receivedMap, wrappedErr: err, } } b, ok := obj.(bool) if ok { if b == false { return false, AssertionError{ failedAssertion: input, received: receivedMap, wrappedErr: fmt.Errorf("expression evaluated to false"), } } return b, nil } return false, AssertionError{ failedAssertion: input, received: receivedMap, wrappedErr: fmt.Errorf("evaluated value is not bool : %v", obj), } } ================================================ FILE: ddosify_engine/core/scenario/scripting/assertion/assert_test.go ================================================ package assertion import ( "errors" "net/http" "reflect" "testing" "go.ddosify.com/ddosify/core/scenario/scripting/assertion/evaluator" ) func TestAssert(t *testing.T) { testHeader := http.Header{} testHeader.Add("Content-Type", "application/json") testHeader.Add("content-length", "222") tests := []struct { input string envs *evaluator.AssertEnv expected bool received map[string]interface{} expectedError string }{ { input: "response_size < 300", envs: &evaluator.AssertEnv{ ResponseSize: 200, }, expected: true, }, { input: "response_size < 300.5", envs: &evaluator.AssertEnv{ ResponseSize: 200, }, expected: true, }, { input: "-response_size < 300.5", envs: &evaluator.AssertEnv{ ResponseSize: 200, }, expected: true, }, { input: "response_time < 300.5", envs: &evaluator.AssertEnv{ ResponseTime: 220, }, expected: true, }, { input: "in(status_code,[200,201])", envs: &evaluator.AssertEnv{ StatusCode: 500, }, expected: false, received: map[string]interface{}{ "status_code": int64(500), "in(status_code,[200,201])": false, }, }, { input: "in(status_code,[200,201])", envs: &evaluator.AssertEnv{ StatusCode: 201, }, expected: true, }, { input: "equals(status_code,200)", envs: &evaluator.AssertEnv{ StatusCode: 200, }, expected: true, }, { input: "status_code == 200", envs: &evaluator.AssertEnv{ StatusCode: 200, }, expected: true, }, { input: "status_code == \"200\"", envs: &evaluator.AssertEnv{ StatusCode: 200, }, expected: true, }, { input: "!(status_code == 200)", envs: &evaluator.AssertEnv{ StatusCode: 200, }, expected: false, received: map[string]interface{}{ "status_code": int64(200), }, }, { input: "not(status_code == 500)", envs: &evaluator.AssertEnv{ StatusCode: 200, }, expected: true, }, { input: `equals(json_path("employees.0.name"),"Kenan")`, envs: &evaluator.AssertEnv{ Body: "{\n \"employees\": [ {\"name\":\"Kursat\"}, {\"name\":\"Kenan\"}]\n}", }, expected: false, received: map[string]interface{}{ "json_path(employees.0.name)": "Kursat", "equals(json_path(employees.0.name),Kenan)": false, }, }, { input: `equals(json_path("employees.1.name"),"Kursat")`, envs: &evaluator.AssertEnv{ Body: "{\n \"employees\": [{\"name\":\"Kenan\"}, {\"name\":\"Kursat\"}]\n}", }, expected: true, }, { input: `exists(headers.Content-Type)`, envs: &evaluator.AssertEnv{ Headers: testHeader, }, expected: true, }, { input: `exists(headers.Not-Exist-Header)`, envs: &evaluator.AssertEnv{ Headers: testHeader, }, expected: false, expectedError: "NotFoundError", }, { input: `contains(body,"xyz")`, envs: &evaluator.AssertEnv{ Body: "xyza", }, expected: true, }, { input: `contains(body,"xyz")`, envs: &evaluator.AssertEnv{ Body: "", }, expected: false, received: map[string]interface{}{ "body": "", "contains(body,xyz)": false, }, }, { input: `regexp(body,"[a-z]+_[0-9]+",0) == "messi_10"`, envs: &evaluator.AssertEnv{ Body: "messi_10alvarez_9", }, expected: true, }, { input: `equals(variables.arr,["Kenan","Faruk","Cakir"])`, envs: &evaluator.AssertEnv{ Variables: map[string]interface{}{ "arr": []interface{}{"Kenan", "Faruk", "Cakir"}, }, }, expected: true, }, { input: `equals(variables.arr,["Kenan","Faruk","Cakir"])`, envs: &evaluator.AssertEnv{ Variables: map[string]interface{}{ "arr2": []interface{}{"Kenan", "Faruk", "Cakir"}, }, }, expected: false, expectedError: "NotFoundError", }, { input: `equals(variables.c,null)`, envs: &evaluator.AssertEnv{ Variables: map[string]interface{}{ "c": nil, }, }, expected: true, }, { input: `variables.arr != ["Kenan","Faruk","Cakir"]`, envs: &evaluator.AssertEnv{ Variables: map[string]interface{}{ "arr": []interface{}{"Cakir"}, }, }, expected: true, }, { input: `variables.arr !=["Kenan","Faruk","Cakir"])`, envs: &evaluator.AssertEnv{ Variables: map[string]interface{}{ "arr": []interface{}{"Kenan", "Faruk", "Cakir"}, }, }, expected: false, received: map[string]interface{}{ "variables.arr": []interface{}{"Kenan", "Faruk", "Cakir"}, }, }, { input: `equals(variables.xint,100)`, // int - int64 comparison envs: &evaluator.AssertEnv{ Variables: map[string]interface{}{ "xint": 100, }, }, expected: true, }, { input: `equals(100,variables.xint)`, // int - int64 comparison envs: &evaluator.AssertEnv{ Variables: map[string]interface{}{ "xint": 100, }, }, expected: true, }, { input: `2*12/3+5-3 != 10`, expected: false, }, { input: `equals(variables.xint,variables.yint)`, // int - int comparison envs: &evaluator.AssertEnv{ Variables: map[string]interface{}{ "xint": 100, "yint": 100, }, }, expected: true, }, { input: `equals(100.5 + 200.5, 301)`, // float64 + envs: &evaluator.AssertEnv{}, expected: true, }, { input: `equals(100.5 - 200.5, -100)`, // float64 - envs: &evaluator.AssertEnv{}, expected: true, }, { input: `equals(4.0 * 10.5, 42)`, // float64 * envs: &evaluator.AssertEnv{}, expected: true, }, { input: `equals(60.0/5, 12)`, // float64 / envs: &evaluator.AssertEnv{}, expected: true, }, { input: `60.1 == 60.1`, // float64 == envs: &evaluator.AssertEnv{}, expected: true, }, { input: `60.1 != 60.1`, // float64 != envs: &evaluator.AssertEnv{}, expected: false, }, { input: `60.1 £ 60.1`, // illegal character envs: &evaluator.AssertEnv{}, expected: false, }, { input: `range(headers.content-length,100,300)`, envs: &evaluator.AssertEnv{ Headers: testHeader, }, expected: true, }, { input: `range(headers.content-length,300,400)`, envs: &evaluator.AssertEnv{ Headers: testHeader, }, expected: false, }, { input: `range(headers.content-length,"300",400)`, envs: &evaluator.AssertEnv{ Headers: testHeader, }, expected: false, expectedError: "ArgumentError", // range params should be integer }, { input: `range(headers.content-length,300,"400")`, envs: &evaluator.AssertEnv{ Headers: testHeader, }, expected: false, expectedError: "ArgumentError", // range params should be integer }, { input: `range(headers.content-length,200,400.2)`, // range can take floats also envs: &evaluator.AssertEnv{ Headers: testHeader, }, expected: true, }, { input: `range(301.2,200,400.2)`, // range can take floats also envs: &evaluator.AssertEnv{ Headers: testHeader, }, expected: true, }, { input: `range(301.2,200,400)`, // range can take floats also envs: &evaluator.AssertEnv{ Headers: testHeader, }, expected: true, }, { input: `equals_on_file("abc","./test_files/a.txt")`, expected: true, }, { input: `equals_on_file("abcx","./test_files/a.txt")`, expected: false, }, { input: `equals_on_file(variables.xyz,"./test_files/jsonMap.json")`, expected: true, envs: &evaluator.AssertEnv{ Variables: map[string]interface{}{ "xyz": map[string]interface{}{ "ask": 130.75, "askSize": float64(10), "averageAnalystRating": "2.0 - Buy", }, }, }, }, { input: `equals_on_file(variables.xyz,"./test_files/jsonArray.json")`, expected: true, envs: &evaluator.AssertEnv{ Variables: map[string]interface{}{ "xyz": []interface{}{"xyz", "abc"}, }, }, }, { input: `equals_on_file(body,"./test_files/currencies.json")`, expected: true, envs: &evaluator.AssertEnv{ Body: "[\n \"AED\",\n \"ARS\",\n \"AUD\",\n \"BGN\",\n \"BHD\",\n \"BRL\",\n \"CAD\",\n \"CHF\",\n \"CNY\",\n \"DKK\",\n \"DZD\",\n \"EUR\",\n \"FKP\",\n \"INR\",\n \"JEP\",\n \"JPY\",\n \"KES\",\n \"KWD\",\n \"KZT\",\n \"MXN\",\n \"NZD\",\n \"RUB\",\n \"SEK\",\n \"SGD\",\n \"TRY\",\n \"USD\"\n]", }, }, { input: "equals(body, {\"name\":\"Ar'gentina\",\"num\":25,\"isChampion\":false})", expected: true, envs: &evaluator.AssertEnv{ Body: "{\"num\":25,\"name\":\"Ar'gentina\",\"isChampion\":false}", }, }, { input: `equals_on_file(body,"./test_files/number.json")`, expected: true, envs: &evaluator.AssertEnv{ Body: "5", }, }, { input: "(status_code == 200) || (status_code == 201)", envs: &evaluator.AssertEnv{ StatusCode: 200, }, expected: true, }, { input: "(status_code == 200) && (status_code == 201)", envs: &evaluator.AssertEnv{ StatusCode: 200, }, expected: false, }, { input: "status_code > variables.envFloatVal", // int float comparison envs: &evaluator.AssertEnv{ StatusCode: 200, Variables: map[string]interface{}{ "envFloatVal": 12.43, }, }, expected: true, }, { input: "status_code && true", envs: &evaluator.AssertEnv{ StatusCode: 200, }, expected: false, expectedError: "OperatorError", // int && bool, unsupported }, { input: "status_code || true", envs: &evaluator.AssertEnv{ StatusCode: 200, }, expected: false, expectedError: "OperatorError", // int || bool, unsupported }, { input: "(status_code > 199) || false", envs: &evaluator.AssertEnv{ StatusCode: 200, }, expected: true, }, { input: "less_than(status_code,201)", envs: &evaluator.AssertEnv{ StatusCode: 200, }, expected: true, }, { input: "greater_than(status_code,201)", envs: &evaluator.AssertEnv{ StatusCode: 400, }, expected: true, }, { input: `range(header.content-length,300,400)`, envs: &evaluator.AssertEnv{ Headers: testHeader, }, expected: false, expectedError: "NotFoundError", // should be headers.... }, { input: "greater_than(status_code,201)", envs: &evaluator.AssertEnv{ StatusCode: 400, }, expected: true, }, { input: `less_than(headers.content-length,500)`, envs: &evaluator.AssertEnv{ Headers: testHeader, }, expected: true, }, { input: "exists(headers.Content-Type2)", envs: &evaluator.AssertEnv{ Headers: testHeader, }, expected: false, }, { input: `in(headers.content-length,[222,445])`, envs: &evaluator.AssertEnv{ Headers: testHeader, }, expected: true, }, { input: "equals(variables.x, -48.880005)", envs: &evaluator.AssertEnv{ Variables: map[string]interface{}{ "x": float64(-48.880005), }, }, expected: true, }, { input: `equals(xpath("//item/title"),"ABC")`, envs: &evaluator.AssertEnv{ Body: ` ABC `, }, expected: true, }, { input: `equals(html_path("//body/h1"),"ABC")`, envs: &evaluator.AssertEnv{ Body: `

ABC

`, }, expected: true, }, { input: "equals(cookies.test.value, \"value\")", envs: &evaluator.AssertEnv{ Cookies: map[string]*http.Cookie{ "test": { Name: "test", Value: "value", }, }, }, expected: true, }, { input: "exists(cookies.test)", envs: &evaluator.AssertEnv{ Cookies: map[string]*http.Cookie{ "test": { Name: "test", Value: "value", }, }, }, expected: true, }, { input: "exists(cookies.test2)", envs: &evaluator.AssertEnv{ Cookies: map[string]*http.Cookie{ "test": { Name: "test", Value: "value", }, }, }, expected: false, }, { input: "cookies.test.secure", envs: &evaluator.AssertEnv{ Cookies: map[string]*http.Cookie{ "test": { Name: "test", Value: "value", Secure: true, }, }, }, expected: true, }, { input: "cookies.test.rawExpires == \"Thu, 01 Jan 1970 00:00:00 GMT\"", envs: &evaluator.AssertEnv{ Cookies: map[string]*http.Cookie{ "test": { Name: "test", Value: "value", Secure: true, RawExpires: "Thu, 01 Jan 1970 00:00:00 GMT", }, }, }, expected: true, }, { input: "cookies.test.path == \"/login\"", envs: &evaluator.AssertEnv{ Cookies: map[string]*http.Cookie{ "test": { Name: "test", Value: "value", Path: "/login", }, }, }, expected: true, }, { input: "cookies.test.expires < time(\"Thu, 01 Jan 1990 00:00:00 GMT\")", envs: &evaluator.AssertEnv{ Cookies: map[string]*http.Cookie{ "test": { Name: "test", Value: "value", Secure: true, RawExpires: "Thu, 01 Jan 1970 00:00:00 GMT", }, }, }, expected: true, }, { input: "cookies.test.httpOnly", envs: &evaluator.AssertEnv{ Cookies: map[string]*http.Cookie{ "test": { Name: "test", Value: "value", HttpOnly: true, }, }, }, expected: true, }, { input: "cookies.test.notexists", envs: &evaluator.AssertEnv{ Cookies: map[string]*http.Cookie{ "test": { Name: "test", Value: "value", HttpOnly: true, }, }, }, expected: false, expectedError: "NotFoundError", }, { input: "cookies.notexists", envs: &evaluator.AssertEnv{ Cookies: map[string]*http.Cookie{ "test": { Name: "test", Value: "value", HttpOnly: true, }, }, }, expected: false, expectedError: "NotFoundError", }, { input: "cookies.notexists.value", envs: &evaluator.AssertEnv{ Cookies: map[string]*http.Cookie{ "test": { Name: "test", Value: "value", HttpOnly: true, }, }, }, expected: false, expectedError: "NotFoundError", }, { input: "cookies.test.maxAge == 100", envs: &evaluator.AssertEnv{ Cookies: map[string]*http.Cookie{ "test": { Name: "test", Value: "value", MaxAge: 100, }, }, }, expected: true, }, { input: "cookies.test.domain == \"ddosify.com\"", envs: &evaluator.AssertEnv{ Cookies: map[string]*http.Cookie{ "test": { Name: "test", Value: "value", Domain: "ddosify.com", }, }, }, expected: true, }, { input: "p99(iteration_duration) == 99", envs: &evaluator.AssertEnv{ TotalTime: []int64{34, 37, 39, 44, 45, 55, 66, 67, 72, 75, 77, 89, 92, 98, 99}, }, expected: true, }, { input: "p98(iteration_duration) == 99", envs: &evaluator.AssertEnv{ TotalTime: []int64{34, 37, 39, 44, 45, 55, 66, 67, 72, 75, 77, 89, 92, 98, 99}, }, expected: true, }, { input: "p95(iteration_duration) == 99", envs: &evaluator.AssertEnv{ TotalTime: []int64{34, 37, 39, 44, 45, 55, 66, 67, 72, 75, 77, 89, 92, 98, 99}, }, expected: true, }, { input: "p90(iteration_duration) == 98", envs: &evaluator.AssertEnv{ TotalTime: []int64{34, 37, 39, 44, 45, 55, 66, 67, 72, 75, 77, 89, 92, 98, 99}, }, expected: true, }, { input: "p80(iteration_duration) == 89", envs: &evaluator.AssertEnv{ TotalTime: []int64{34, 37, 39, 44, 45, 55, 66, 67, 72, 75, 77, 89, 92, 98, 99}, }, expected: true, }, { input: "min(iteration_duration) == 34", envs: &evaluator.AssertEnv{ TotalTime: []int64{34, 37, 39, 44, 45, 55, 66, 67, 72, 75, 77, 89, 92, 98, 99}, }, expected: true, }, { input: "max(iteration_duration) == 99", envs: &evaluator.AssertEnv{ TotalTime: []int64{34, 37, 39, 44, 45, 55, 66, 67, 72, 75, 77, 89, 92, 98, 99}, }, expected: true, }, { input: "max(iteration_duration) == 2222", envs: &evaluator.AssertEnv{ TotalTime: []int64{34, 37, 39, 44, 45, 55, 66, 67, 2222, 72, 75, 77, 89, 92, 98, 99}, }, expected: true, }, { input: "avg(iteration_duration) == 200.6875", envs: &evaluator.AssertEnv{ TotalTime: []int64{34, 37, 39, 44, 45, 55, 66, 67, 2222, 72, 75, 77, 89, 92, 98, 99}, }, expected: true, }, { input: "percentile([]) == 200.6875", expected: false, }, { input: "min([]) == 200.6875", expected: false, }, { input: "max([]) == 200.6875", expected: false, }, { input: "avg(response_size) == 200.6875", envs: &evaluator.AssertEnv{ ResponseSize: int64(23), }, expected: false, }, { input: "not(response_size)", envs: &evaluator.AssertEnv{ ResponseSize: int64(23), }, expected: false, expectedError: "ArgumentError", }, { input: "less_than(10, 20.3)", envs: &evaluator.AssertEnv{ ResponseSize: int64(23), }, expected: false, expectedError: "ArgumentError", }, { input: `equals_on_file("abc", [34,60])`, // filepath must be string expected: false, expectedError: "ArgumentError", }, { input: "in(response_size,response_size)", // second arg must be array envs: &evaluator.AssertEnv{ ResponseSize: int64(23), }, expected: false, expectedError: "ArgumentError", }, { input: "json_path(23)", // arg must be string expected: false, expectedError: "ArgumentError", }, { input: "xpath(23)", // arg must be string expected: false, expectedError: "ArgumentError", }, { input: "html_path(23)", // arg must be string expected: false, expectedError: "ArgumentError", }, { input: "p99(23)", // arg must be array expected: false, expectedError: "ArgumentError", }, { input: "p98(23)", // arg must be array expected: false, expectedError: "ArgumentError", }, { input: "p95(23)", // arg must be array expected: false, expectedError: "ArgumentError", }, { input: "p90(23)", // arg must be array expected: false, expectedError: "ArgumentError", }, { input: "p80(23)", // arg must be array expected: false, expectedError: "ArgumentError", }, { input: "p80([])", // empty array expected: false, }, { input: "min([])", // empty array expected: false, }, { input: "max([])", // empty array expected: false, }, { input: "avg([])", // empty interface array, not []int64 expected: false, }, } for _, tc := range tests { t.Run(tc.input, func(t *testing.T) { eval, err := Assert(tc.input, tc.envs) if tc.expected != eval { t.Errorf("assert expected %t", tc.expected) t.Log(err) } if err != nil && tc.expectedError != "" { if tc.expectedError == "NotFoundError" { var notFoundError evaluator.NotFoundError if !errors.As(err, ¬FoundError) { t.Errorf("Should be evaluator.NotFoundError, got %v", err) } } else if tc.expectedError == "ArgumentError" { var argError evaluator.ArgumentError if !errors.As(err, &argError) { t.Errorf("Should be evaluator.ArgumentError, got %v", err) } } else if tc.expectedError == "OperatorError" { var opError evaluator.OperatorError if !errors.As(err, &opError) { t.Errorf("Should be evaluator.OperatorError, got %v", err) } } } if err != nil && tc.received != nil { assertErr := err.(AssertionError) if !reflect.DeepEqual(assertErr.Received(), tc.received) { t.Errorf("received expected %v, got %v", tc.received, assertErr.Received()) } } }) } } ================================================ FILE: ddosify_engine/core/scenario/scripting/assertion/ast/ast.go ================================================ package ast import ( "bytes" "strings" "go.ddosify.com/ddosify/core/scenario/scripting/assertion/token" ) // The base Node interface type Node interface { TokenLiteral() string String() string } // All statement nodes implement this type Statement interface { Node statementNode() } // All expression nodes implement this type Expression interface { Node expressionNode() } type ExpressionStatement struct { Token token.Token // the first token of the expression Expression Expression } func (es *ExpressionStatement) statementNode() {} func (es *ExpressionStatement) TokenLiteral() string { return es.Token.Literal } func (es *ExpressionStatement) String() string { if es.Expression != nil { return es.Expression.String() } return "" } // Expressions type Identifier struct { Token token.Token // the token.IDENT token Value string } func (i *Identifier) expressionNode() {} func (i *Identifier) TokenLiteral() string { return i.Token.Literal } func (i *Identifier) String() string { return i.Value } type Boolean struct { Token token.Token Value bool } func (b *Boolean) expressionNode() {} func (b *Boolean) TokenLiteral() string { return b.Token.Literal } func (b *Boolean) String() string { return b.Token.Literal } func (il *Boolean) GetVal() interface{} { return il.Value } type IntegerLiteral struct { Token token.Token Value int64 } func (il *IntegerLiteral) expressionNode() {} func (il *IntegerLiteral) TokenLiteral() string { return il.Token.Literal } func (il *IntegerLiteral) String() string { return il.Token.Literal } func (il *IntegerLiteral) GetVal() interface{} { return il.Value } type FloatLiteral struct { Token token.Token Value float64 } func (il *FloatLiteral) expressionNode() {} func (il *FloatLiteral) TokenLiteral() string { return il.Token.Literal } func (il *FloatLiteral) String() string { return il.Token.Literal } func (il *FloatLiteral) GetVal() interface{} { return il.Value } type NullLiteral struct { Token token.Token Value interface{} } func (il *NullLiteral) expressionNode() {} func (il *NullLiteral) TokenLiteral() string { return il.Token.Literal } func (il *NullLiteral) String() string { return il.Token.Literal } func (il *NullLiteral) GetVal() interface{} { return il.Value } type StringLiteral struct { Token token.Token Value string } func (il *StringLiteral) expressionNode() {} func (il *StringLiteral) TokenLiteral() string { return il.Token.Literal } func (il *StringLiteral) String() string { return il.Token.Literal } func (il *StringLiteral) GetVal() interface{} { return il.Value } type ArrayLiteral struct { Token token.Token Elems []Expression } func (il *ArrayLiteral) expressionNode() {} func (il *ArrayLiteral) TokenLiteral() string { return il.Token.Literal } func (il *ArrayLiteral) String() string { x := []string{} for _, e := range il.Elems { x = append(x, e.String()) } return "[" + strings.Join(x, ",") + "]" } type ObjectLiteral struct { Token token.Token Elems map[string]Expression } func (il *ObjectLiteral) expressionNode() {} func (il *ObjectLiteral) TokenLiteral() string { return il.Token.Literal } func (il *ObjectLiteral) String() string { x := []string{} for k, e := range il.Elems { x = append(x, k+":"+e.String()) } return "{" + strings.Join(x, ",") + "}" } type PrefixExpression struct { Token token.Token // The prefix token, e.g. ! Operator string Right Expression } func (pe *PrefixExpression) expressionNode() {} func (pe *PrefixExpression) TokenLiteral() string { return pe.Token.Literal } func (pe *PrefixExpression) String() string { var out bytes.Buffer out.WriteString("(") out.WriteString(pe.Operator) out.WriteString(pe.Right.String()) out.WriteString(")") return out.String() } type InfixExpression struct { Token token.Token // The operator token, e.g. + Left Expression Operator string Right Expression } func (oe *InfixExpression) expressionNode() {} func (oe *InfixExpression) TokenLiteral() string { return oe.Token.Literal } func (oe *InfixExpression) String() string { var out bytes.Buffer out.WriteString("(") out.WriteString(oe.Left.String()) out.WriteString(" " + oe.Operator + " ") out.WriteString(oe.Right.String()) out.WriteString(")") return out.String() } type CallExpression struct { Token token.Token // The '(' token Function Expression // Identifier or FunctionLiteral Arguments []Expression } func (ce *CallExpression) expressionNode() {} func (ce *CallExpression) TokenLiteral() string { return ce.Token.Literal } func (ce *CallExpression) String() string { var out bytes.Buffer args := []string{} for _, a := range ce.Arguments { args = append(args, a.String()) } out.WriteString(ce.Function.String()) out.WriteString("(") out.WriteString(strings.Join(args, ",")) out.WriteString(")") return out.String() } ================================================ FILE: ddosify_engine/core/scenario/scripting/assertion/evaluator/env.go ================================================ package evaluator import "net/http" type AssertEnv struct { StatusCode int64 ResponseSize int64 ResponseTime int64 // in ms Body string Headers http.Header Variables map[string]interface{} Cookies map[string]*http.Cookie // cookies sent by the server, name -> cookie // For test-wide assertions TotalTime []int64 // in ms FailCount int FailCountPerc float64 // should be in range [0,1] } ================================================ FILE: ddosify_engine/core/scenario/scripting/assertion/evaluator/evaluator.go ================================================ package evaluator import ( "encoding/json" "fmt" "net/http" "reflect" "strconv" "strings" "time" "go.ddosify.com/ddosify/core/scenario/scripting/assertion/ast" ) func Eval(node ast.Node, env *AssertEnv, receivedMap map[string]interface{}) (interface{}, error) { switch node := node.(type) { case *ast.ExpressionStatement: return Eval(node.Expression, env, receivedMap) // Expressions case *ast.IntegerLiteral: return node.GetVal(), nil case *ast.FloatLiteral: return node.GetVal(), nil case *ast.StringLiteral: return node.GetVal(), nil case *ast.NullLiteral: return node.GetVal(), nil case *ast.ArrayLiteral: args, err := evalExpressions(node.Elems, env, receivedMap) if err != nil { return nil, err } return args, nil case *ast.ObjectLiteral: args, err := evalObjectExpressions(node.Elems, env, receivedMap) if err != nil { return nil, err } return args, nil case *ast.Boolean: return node.GetVal(), nil case *ast.PrefixExpression: right, err := Eval(node.Right, env, receivedMap) if err != nil { return nil, err } return evalPrefixExpression(node.Operator, right) case *ast.InfixExpression: left, err := Eval(node.Left, env, receivedMap) if err != nil { return nil, err } right, err := Eval(node.Right, env, receivedMap) if err != nil { return nil, err } return evalInfixExpression(node.Operator, left, right) case *ast.Identifier: return evalIdentifier(node, env, receivedMap) case *ast.CallExpression: funcName := node.Function.(*ast.Identifier).Value if _, ok := assertionFuncMap[funcName]; ok { args, err := evalExpressions(node.Arguments, env, receivedMap) if err != nil { return false, err } f := func() (result interface{}, err error) { defer func() { if r := recover(); r != nil { result = nil err = fmt.Errorf("probably error during type conversion , %v", r) } }() switch funcName { case NOT: p, ok := args[0].(bool) if !ok { return false, ArgumentError{ msg: "arg of not func must be a bool", wrappedErr: nil, } } return not(p), nil case LESSTHAN: variable, ok := args[0].(int64) if !ok { variable, _ = strconv.ParseInt(args[0].(string), 0, 64) } limit, ok := args[1].(int64) if !ok { return false, ArgumentError{ msg: "limit of less_than should be integer", wrappedErr: nil, } } return less_than(variable, limit), nil case GREATERTHAN: variable, ok := args[0].(int64) if !ok { variable, _ = strconv.ParseInt(args[0].(string), 0, 64) } limit, ok := args[1].(int64) if !ok { return false, ArgumentError{ msg: "limit of greater_than should be integer", wrappedErr: nil, } } return greater_than(variable, limit), nil case EQUALS: return equals(args[0], args[1]) case EQUALSONFILE: filepath, ok := args[1].(string) if !ok { return false, ArgumentError{ msg: "filepath must be a string", wrappedErr: nil, } } return equalsOnFile(args[0], filepath) case IN: elems, ok := args[1].([]interface{}) if !ok { return false, ArgumentError{ msg: "second arg of in func must be an array", wrappedErr: nil, } } return in(args[0], elems) case JSONPATH: jsonpath, ok := args[0].(string) if !ok { return false, ArgumentError{ msg: "jsonpath must be a string", wrappedErr: nil, } } return jsonExtract(env.Body, jsonpath) case XMLPATH: xpath, ok := args[0].(string) if !ok { return false, ArgumentError{ msg: "xpath must be a string", wrappedErr: nil, } } return xmlExtract(env.Body, xpath) case HTMLPATH: html, ok := args[0].(string) if !ok { return false, ArgumentError{ msg: "htmlpath must be a string", wrappedErr: nil, } } return htmlExtract(env.Body, html) case REGEXP: regexp, ok := args[1].(string) if !ok { return false, ArgumentError{ msg: "regexp must be a string", wrappedErr: nil, } } matchNo, ok := args[2].(int64) if !ok { return false, ArgumentError{ msg: "matchNo must be an int64", wrappedErr: nil, } } return regexExtract(env.Body, regexp, matchNo) case EXISTS: if args[0] != nil { return true, nil // if identifier evaluated, and exists } return false, nil case TIME: return timeF(args[0].(string)) case CONTAINS: p1, ok := args[0].(string) if !ok { return false, ArgumentError{ msg: "args of contains func must be string", wrappedErr: nil, } } p2, ok := args[1].(string) if !ok { return false, ArgumentError{ msg: "args of contains func must be string", wrappedErr: nil, } } return contains(p1, p2), nil case AVG: arr, ok := args[0].([]int64) if !ok { return false, ArgumentError{ msg: "argument of avg func must be an int64 array", wrappedErr: nil, } } return avg(arr) case MIN: arr, ok := args[0].([]int64) if !ok { return false, ArgumentError{ msg: "argument of min func must be an int64 array", wrappedErr: nil, } } return min(arr) case MAX: arr, ok := args[0].([]int64) if !ok { return false, ArgumentError{ msg: "argument of max func must be an int64 array", wrappedErr: nil, } } return max(arr) // TODO only one func percentile(arr, num) ? case P99: arr, ok := args[0].([]int64) if !ok { return false, ArgumentError{ msg: "argument of percentile funcs must be an int64 array", wrappedErr: nil, } } return percentile(arr, 99) case P98: arr, ok := args[0].([]int64) if !ok { return false, ArgumentError{ msg: "argument of percentile funcs must be an int64 array", wrappedErr: nil, } } return percentile(arr, 98) case P95: arr, ok := args[0].([]int64) if !ok { return false, ArgumentError{ msg: "argument of percentile funcs must be an int64 array", wrappedErr: nil, } } return percentile(arr, 95) case P90: arr, ok := args[0].([]int64) if !ok { return false, ArgumentError{ msg: "argument of percentile funcs must be an int64 array", wrappedErr: nil, } } return percentile(arr, 90) case P80: arr, ok := args[0].([]int64) if !ok { return false, ArgumentError{ msg: "argument of percentile funcs must be an int64 array", wrappedErr: nil, } } return percentile(arr, 80) case RANGE: var x, low, high float64 x, ok = args[0].(float64) if !ok { xInt, ok := args[0].(int64) if ok { x = float64(xInt) } else { // assume that it is string xFloat, err := strconv.ParseFloat(args[0].(string), 64) if err != nil { return false, ArgumentError{ msg: "arguments of range should be integer or float", wrappedErr: nil, } } x = xFloat } } low, ok = args[1].(float64) if !ok { lowInt, ok := args[1].(int64) if !ok { return false, ArgumentError{ msg: "arguments of range should be integer or float", wrappedErr: nil, } } low = float64(lowInt) } high, ok = args[2].(float64) if !ok { highInt, ok := args[2].(int64) if !ok { return false, ArgumentError{ msg: "arguments of range should be integer or float", wrappedErr: nil, } } high = float64(highInt) } return rangeF(x, low, high), nil } return nil, NotFoundError{ source: fmt.Sprintf("func %s not defined", funcName), wrappedErr: nil, } } res, err := f() receivedMap[node.String()] = res return res, err } } return nil, nil } func evalPrefixExpression(operator string, right interface{}) (interface{}, error) { switch operator { case "!": return evalBangOperatorExpression(right) case "-": return evalMinusPrefixOperatorExpression(right) default: return nil, OperatorError{ msg: fmt.Sprintf("unknown operator: %s%s", operator, right), wrappedErr: nil, } } } func evalInfixExpression( operator string, left, right interface{}, ) (interface{}, error) { leftType := reflect.ValueOf(left).Kind() rightType := reflect.ValueOf(right).Kind() // int - int if leftType == reflect.Int64 && rightType == reflect.Int64 { return evalIntegerInfixExpression(operator, left.(int64), right.(int64)) } if leftType == reflect.Int64 && rightType == reflect.Int { return evalIntegerInfixExpression(operator, left.(int64), int64(right.(int))) } if leftType == reflect.Int && rightType == reflect.Int64 { return evalIntegerInfixExpression(operator, int64(left.(int)), right.(int64)) } if leftType == reflect.Int && rightType == reflect.Int { return evalIntegerInfixExpression(operator, int64(left.(int)), int64(left.(int))) } // int - float, convert int64 to float64, data loss for big int64 numbers if leftType == reflect.Int64 && rightType == reflect.Float64 { return evalFloatInfixExpression(operator, float64(left.(int64)), right.(float64)) } if leftType == reflect.Float64 && rightType == reflect.Int64 { return evalFloatInfixExpression(operator, left.(float64), float64(right.(int64))) } // float - float if leftType == reflect.Float64 && rightType == reflect.Float64 { return evalFloatInfixExpression(operator, left.(float64), right.(float64)) } // string - int if leftType == reflect.String && rightType == reflect.Int64 { leftInt, _ := strconv.ParseInt(left.(string), 0, 64) return evalIntegerInfixExpression(operator, leftInt, right.(int64)) } if leftType == reflect.Int64 && rightType == reflect.String { rightInt, _ := strconv.ParseInt(right.(string), 0, 64) return evalIntegerInfixExpression(operator, left.(int64), rightInt) } if lTime, lok := left.(time.Time); lok { if rTime, rok := right.(time.Time); rok { return evalTimeInfixExpression(operator, lTime, rTime) } return nil, OperatorError{ msg: "time can be only compared with time", } } // other types if leftType == reflect.String && rightType == reflect.String { // json marshalling is used to compare json strings var lJson, rJson interface{} isLJson := json.Unmarshal([]byte(left.(string)), &lJson) isRJson := json.Unmarshal([]byte(right.(string)), &rJson) if isLJson == nil && isRJson == nil { return reflect.DeepEqual(lJson, rJson), nil } } if leftType == reflect.String && rightType == reflect.Map { var lJson interface{} isLJson := json.Unmarshal([]byte(left.(string)), &lJson) if isLJson == nil { rJsonBy, _ := json.Marshal(right) lJsonBy, _ := json.Marshal(lJson) return reflect.DeepEqual(rJsonBy, lJsonBy), nil } } if leftType == reflect.Map && rightType == reflect.String { var rJson interface{} isRJson := json.Unmarshal([]byte(right.(string)), &rJson) if isRJson == nil { lJsonBy, _ := json.Marshal(left) rJsonBy, _ := json.Marshal(rJson) return reflect.DeepEqual(lJsonBy, rJsonBy), nil } } if operator == "==" { return reflect.DeepEqual(left, right), nil } if operator == "!=" { return !reflect.DeepEqual(left, right), nil } if operator == "&&" { if leftType == reflect.Bool && rightType == reflect.Bool { return left.(bool) && right.(bool), nil } return nil, OperatorError{ msg: fmt.Sprintf("operator && unsupported for types: %s and %s", leftType, rightType), wrappedErr: nil, } } if operator == "||" { if leftType == reflect.Bool && rightType == reflect.Bool { return left.(bool) || right.(bool), nil } return nil, OperatorError{ msg: fmt.Sprintf("operator || unsupported for types: %s and %s", leftType, rightType), wrappedErr: nil, } } return nil, OperatorError{ msg: fmt.Sprintf("unknown operator: evalInfixExpression %s", operator), wrappedErr: nil, } } func evalBangOperatorExpression(right interface{}) (bool, error) { b, ok := right.(bool) if ok { return !b, nil } return false, OperatorError{ msg: fmt.Sprintf("identifier before ! operator must be bool, %s", right), wrappedErr: nil, } } func evalMinusPrefixOperatorExpression(right interface{}) (interface{}, error) { i, ok := right.(int64) if ok { return -i, nil } var j float64 j, ok = right.(float64) if ok { return -j, nil } if !ok { return 0, OperatorError{ msg: fmt.Sprintf("- operator not applicable for %v", right), wrappedErr: nil, } } return -i, nil } func evalFloatInfixExpression(operator string, left, right float64, ) (interface{}, error) { switch operator { case "+": return left + right, nil case "-": return left - right, nil case "*": return left * right, nil case "/": return left / right, nil case "<": return left < right, nil case ">": return left > right, nil case "==": return left == right, nil case "!=": return left != right, nil default: return 0, OperatorError{ msg: fmt.Sprintf("unknown operator %s for floats", operator), wrappedErr: nil, } } } func evalTimeInfixExpression(operator string, lTime, rTime time.Time) (interface{}, error) { switch operator { case "==": return lTime == rTime, nil case "!=": return lTime != rTime, nil case "<": return lTime.Before(rTime), nil case ">": return lTime.After(rTime), nil default: return 0, OperatorError{ msg: fmt.Sprintf("unknown operator %s for time.Time", operator), wrappedErr: nil, } } } func evalIntegerInfixExpression( operator string, left, right int64, ) (interface{}, error) { switch operator { case "+": return left + right, nil case "-": return left - right, nil case "*": return left * right, nil case "/": return left / right, nil case "<": return left < right, nil case ">": return left > right, nil case "==": return left == right, nil case "!=": return left != right, nil default: return 0, OperatorError{ msg: fmt.Sprintf("unknown operator %s for integers", operator), wrappedErr: nil, } } } func evalIdentifier( node *ast.Identifier, env *AssertEnv, receivedMap map[string]interface{}, ) (interface{}, error) { ident := node.Value if strings.EqualFold(ident, "status_code") { receivedMap[ident] = env.StatusCode return env.StatusCode, nil } if strings.EqualFold(ident, "response_size") { receivedMap[ident] = env.ResponseSize return env.ResponseSize, nil } if strings.EqualFold(ident, "response_time") { receivedMap[ident] = env.ResponseTime return env.ResponseTime, nil } if strings.EqualFold(ident, "body") { receivedMap[ident] = env.Body return env.Body, nil } // test-wide identifiers if strings.EqualFold(ident, "fail_count") { receivedMap[ident] = env.FailCount return env.FailCount, nil } if strings.EqualFold(ident, "fail_count_perc") { receivedMap[ident] = env.FailCountPerc return env.FailCountPerc, nil } if strings.EqualFold(ident, "iteration_duration") { receivedMap[ident] = env.TotalTime return env.TotalTime, nil } if strings.HasPrefix(ident, "variables.") { vr := strings.TrimPrefix(ident, "variables.") if v, ok := env.Variables[vr]; ok { receivedMap[ident] = v return v, nil } return "", NotFoundError{ source: fmt.Sprintf("variable not found %s", vr), wrappedErr: nil, } } if strings.HasPrefix(ident, "headers.") { vr := strings.TrimPrefix(ident, "headers.") hv := env.Headers.Get(vr) if hv != "" { receivedMap[ident] = hv return hv, nil } return "", NotFoundError{ // source: fmt.Sprintf("header not found %s", vr), wrappedErr: nil, } } if strings.HasPrefix(ident, "cookies.") { // cookies.cookie_name.field_name // cookies.csrftoken.expires vr := strings.TrimPrefix(ident, "cookies.") words := strings.Split(vr, ".") // e.g. ["csrftoken", "expires"] or ["csrftoken"] if len(words) == 1 { name := words[0] if v, ok := env.Cookies[name]; ok { receivedMap[ident] = v return v, nil } return "", NotFoundError{ // source: fmt.Sprintf("cookie not found %s", name), wrappedErr: nil, } } else if len(words) == 2 { name := words[0] if v, ok := env.Cookies[name]; ok { fieldName := words[1] value, err := evalCookieField(v, fieldName) if err != nil { return "", NotFoundError{ // source: fmt.Sprintf("cookie field not found %s", fieldName), wrappedErr: err, } } receivedMap[ident] = value return value, nil } else { return "", NotFoundError{ // source: fmt.Sprintf("cookie not found %s", name), wrappedErr: nil, } } } } return "", NotFoundError{ // source: fmt.Sprintf("%s not defined", ident), wrappedErr: nil, } } func evalObjectExpressions( exps map[string]ast.Expression, env *AssertEnv, receivedMap map[string]interface{}, ) (map[string]interface{}, error) { var result = make(map[string]interface{}) for k, e := range exps { evaluated, err := Eval(e, env, receivedMap) if err != nil { return nil, err } switch e.(type) { case *ast.Identifier: receivedMap[e.String()] = evaluated case *ast.CallExpression: receivedMap[e.String()] = evaluated } result[k] = evaluated } return result, nil } func evalExpressions( exps []ast.Expression, env *AssertEnv, receivedMap map[string]interface{}, ) ([]interface{}, error) { var result = make([]interface{}, 0) for _, e := range exps { evaluated, err := Eval(e, env, receivedMap) if err != nil { return nil, err } switch e.(type) { case *ast.Identifier: receivedMap[e.String()] = evaluated case *ast.CallExpression: receivedMap[e.String()] = evaluated } result = append(result, evaluated) } return result, nil } func evalCookieField(c *http.Cookie, fieldName string) (interface{}, error) { switch fieldName { case "name": return c.Name, nil case "value": return c.Value, nil case "path": return c.Path, nil case "domain": return c.Domain, nil case "expires": return c.Expires, nil case "rawExpires": return c.RawExpires, nil case "maxAge": return c.MaxAge, nil case "secure": return c.Secure, nil case "httpOnly": return c.HttpOnly, nil case "raw": return c.Raw, nil // case "unparsed": // return c.Unparsed, nil default: return nil, fmt.Errorf("unknown field %s", fieldName) } } type NotFoundError struct { // UnWrappable source string wrappedErr error } func (nf NotFoundError) Error() string { return fmt.Sprintf("%s", nf.source) } func (nf NotFoundError) Unwrap() error { return nf.wrappedErr } type ArgumentError struct { // UnWrappable msg string wrappedErr error } func (nf ArgumentError) Error() string { return fmt.Sprintf("%s", nf.msg) } func (nf ArgumentError) Unwrap() error { return nf.wrappedErr } type OperatorError struct { // UnWrappable msg string wrappedErr error } func (nf OperatorError) Error() string { return fmt.Sprintf("%s", nf.msg) } func (nf OperatorError) Unwrap() error { return nf.wrappedErr } ================================================ FILE: ddosify_engine/core/scenario/scripting/assertion/evaluator/function.go ================================================ package evaluator import ( "encoding/json" "fmt" "math" "os" "reflect" "strings" "time" "go.ddosify.com/ddosify/core/scenario/scripting/extraction" "go.ddosify.com/ddosify/core/types" ) var less_than = func(variable int64, limit int64) bool { return variable < limit } var greater_than = func(variable int64, limit int64) bool { return variable > limit } var not = func(b bool) bool { return !b } // assumed given array is sorted var percentile = func(arr []int64, num int) (int64, error) { if len(arr) == 0 { return 0, fmt.Errorf("empty input array on percentile func") } index := int(math.Ceil(float64(len(arr)*num)/100)) - 1 if index < 0 { index = 0 } return arr[index], nil } var min = func(arr []int64) (int64, error) { if len(arr) == 0 { return 0, fmt.Errorf("empty input array on min func") } min := arr[0] for _, i := range arr { if min > i { min = i } } return min, nil } var max = func(arr []int64) (int64, error) { if len(arr) == 0 { return 0, fmt.Errorf("empty input array on max func") } max := arr[0] for _, i := range arr { if max < i { max = i } } return max, nil } var avg = func(arr []int64) (float64, error) { if len(arr) == 0 { return 0, fmt.Errorf("empty input array on avg func") } var total int64 for _, i := range arr { total += i } return float64(total) / float64(len(arr)), nil } var equals = func(a, b interface{}) (bool, error) { b, err := evalInfixExpression("==", a, b) if err != nil { return false, err } return b.(bool), nil } var in = func(a interface{}, b []interface{}) (bool, error) { for _, elem := range b { if eq, err := equals(a, elem); eq { return true, nil } else if err != nil { return false, err } } return false, nil } var contains = func(source string, substr string) bool { if strings.Contains(source, substr) { return true } return false } var timeF = func(t string) (time.Time, error) { res, err := time.Parse(time.RFC1123, t) if err != nil { return time.Time{}, err } return res, nil } var rangeF = func(x float64, low float64, hi float64) bool { if x >= low && x < hi { return true } return false } var jsonExtract = func(source interface{}, jsonPath string) (interface{}, error) { val, err := extraction.ExtractFromJson(source, jsonPath) return val, err } var xmlExtract = func(source interface{}, xPath string) (interface{}, error) { val, err := extraction.ExtractFromXml(source, xPath) return val, err } var htmlExtract = func(source interface{}, xPath string) (interface{}, error) { val, err := extraction.ExtractFromHtml(source, xPath) return val, err } var regexExtract = func(source interface{}, xPath string, matchNo int64) (interface{}, error) { val, err := extraction.ExtractWithRegex(source, types.RegexCaptureConf{ Exp: &xPath, No: int(matchNo), }) return val, err } var equalsOnFile = func(source interface{}, filepath string) (bool, error) { fileBytes, err := os.ReadFile(filepath) if err != nil { return false, err } if strings.HasSuffix(filepath, ".json") { sourceType := reflect.ValueOf(source).Kind() // json extracted types may be map or slice etc if sourceType == reflect.String { // in case of direct body comparison, source param will be string var src interface{} err := json.Unmarshal([]byte(source.(string)), &src) if err != nil { return false, err } var fileB interface{} err = json.Unmarshal(fileBytes, &fileB) if err != nil { return false, err } if reflect.DeepEqual(src, fileB) { return true, nil } } var fs interface{} json.Unmarshal(fileBytes, &fs) if reflect.DeepEqual(source, fs) { return true, nil } return false, nil } if fmt.Sprint(source) == string(fileBytes) { return true, nil } return false, nil } var assertionFuncMap = map[string]struct{}{ NOT: {}, LESSTHAN: {}, GREATERTHAN: {}, EQUALS: {}, EQUALSONFILE: {}, IN: {}, JSONPATH: {}, XMLPATH: {}, HTMLPATH: {}, REGEXP: {}, EXISTS: {}, CONTAINS: {}, RANGE: {}, MIN: {}, MAX: {}, AVG: {}, P99: {}, P98: {}, P95: {}, P90: {}, P80: {}, TIME: {}, } const ( NOT = "not" LESSTHAN = "less_than" GREATERTHAN = "greater_than" EQUALS = "equals" IN = "in" JSONPATH = "json_path" XMLPATH = "xpath" HTMLPATH = "html_path" REGEXP = "regexp" EXISTS = "exists" CONTAINS = "contains" RANGE = "range" EQUALSONFILE = "equals_on_file" TIME = "time" MIN = "min" MAX = "max" AVG = "avg" P99 = "p99" P98 = "p98" P95 = "p95" P90 = "p90" P80 = "p80" ) ================================================ FILE: ddosify_engine/core/scenario/scripting/assertion/evaluator/function_test.go ================================================ package evaluator import "testing" func TestEmptyArraysOnMinMaxAvgFuncs(t *testing.T) { empty := []int64{} _, err := min(empty) if err == nil { t.Errorf("expected error on empty array on min func") } _, err = max(empty) if err == nil { t.Errorf("expected error on empty array on max func") } _, err = avg(empty) if err == nil { t.Errorf("expected error on empty array on avg func") } _, err = percentile(empty, 99) if err == nil { t.Errorf("expected error on empty array on percentile func") } } ================================================ FILE: ddosify_engine/core/scenario/scripting/assertion/lexer/lexer.go ================================================ package lexer import ( "strings" "go.ddosify.com/ddosify/core/scenario/scripting/assertion/token" ) type Lexer struct { input string position int // current position in input (points to current char) readPosition int // current reading position in input (after current char) ch byte // current char under examination } func New(input string) *Lexer { l := &Lexer{input: input} l.readChar() return l } func (l *Lexer) NextToken() token.Token { var tok token.Token l.skipWhitespace() switch l.ch { case '=': if l.peekChar() == '=' { ch := l.ch l.readChar() literal := string(ch) + string(l.ch) tok = token.Token{Type: token.EQ, Literal: literal} } else { tok = newToken(token.ILLEGAL, l.ch) } case '&': if l.peekChar() == '&' { ch := l.ch l.readChar() literal := string(ch) + string(l.ch) tok = token.Token{Type: token.AND, Literal: literal} } else { tok = newToken(token.ILLEGAL, l.ch) } case '|': if l.peekChar() == '|' { ch := l.ch l.readChar() literal := string(ch) + string(l.ch) tok = token.Token{Type: token.OR, Literal: literal} } else { tok = newToken(token.ILLEGAL, l.ch) } case '+': tok = newToken(token.PLUS, l.ch) case '-': tok = newToken(token.MINUS, l.ch) case '!': if l.peekChar() == '=' { ch := l.ch l.readChar() literal := string(ch) + string(l.ch) tok = token.Token{Type: token.NOT_EQ, Literal: literal} } else { tok = newToken(token.BANG, l.ch) } case '/': tok = newToken(token.SLASH, l.ch) case '*': tok = newToken(token.ASTERISK, l.ch) case '<': tok = newToken(token.LT, l.ch) case '>': tok = newToken(token.GT, l.ch) case ',': tok = newToken(token.COMMA, l.ch) case '(': tok = newToken(token.LPAREN, l.ch) case ')': tok = newToken(token.RPAREN, l.ch) case '{': tok = newToken(token.LBRACE, l.ch) case '}': tok = newToken(token.RBRACE, l.ch) case '[': tok = newToken(token.LBRACKET, l.ch) case ']': tok = newToken(token.RBRACKET, l.ch) case ':': tok = newToken(token.COLON, l.ch) case 0: tok.Literal = "" tok.Type = token.EOF default: if l.ch == 34 { // " l.readChar() tok.Literal = l.readString() tok.Type = token.STRING l.readChar() return tok } if l.ch == 39 { // ' l.readChar() tok.Literal = l.readRawString() tok.Type = token.STRING l.readChar() return tok } if isDigit(l.ch) { // number tok.Type = token.INT tok.Literal = l.readNumber() if strings.Contains(tok.Literal, ".") { if strings.Count(tok.Literal, ".") > 1 { // more than 1 . is illegal tok.Type = token.ILLEGAL } tok.Type = token.FLOAT } else { tok.Type = token.INT } return tok } else if isLetter(l.ch) { // identifier tok.Literal = l.readIdentifier() tok.Type = token.LookupIdent(tok.Literal) return tok } else { tok = newToken(token.ILLEGAL, l.ch) } } l.readChar() return tok } func (l *Lexer) skipWhitespace() { for l.ch == ' ' || l.ch == '\t' || l.ch == '\n' || l.ch == '\r' { l.readChar() } } func (l *Lexer) readChar() { if l.readPosition >= len(l.input) { l.ch = 0 // ASCII code for NUL char } else { l.ch = l.input[l.readPosition] } l.position = l.readPosition l.readPosition += 1 } func (l *Lexer) peekChar() byte { if l.readPosition >= len(l.input) { return 0 } else { return l.input[l.readPosition] } } func (l *Lexer) readIdentifier() string { position := l.position for isChAllowedInIdent(l.ch) { l.readChar() } return l.input[position:l.position] } func (l *Lexer) readString() string { position := l.position for l.ch != 34 { // " l.readChar() } return l.input[position:l.position] } func (l *Lexer) readRawString() string { position := l.position for l.ch != 39 { // ' l.readChar() } return l.input[position:l.position] } func (l *Lexer) readNumber() string { position := l.position for isDigit(l.ch) { l.readChar() } return l.input[position:l.position] } func isChAllowedInIdent(ch byte) bool { return 'a' <= ch && ch <= 'z' || 'A' <= ch && ch <= 'Z' || ch == '_' || ch == '.' || ch == '-' || '0' <= ch && ch <= '9' } func isLetter(ch byte) bool { // identifiers return 'a' <= ch && ch <= 'z' || 'A' <= ch && ch <= 'Z' } func isDigit(ch byte) bool { return '0' <= ch && ch <= '9' || ch == '.' } func newToken(tokenType token.TokenType, ch byte) token.Token { return token.Token{Type: tokenType, Literal: string(ch)} } ================================================ FILE: ddosify_engine/core/scenario/scripting/assertion/lexer/lexer_test.go ================================================ package lexer import ( "testing" "go.ddosify.com/ddosify/core/scenario/scripting/assertion/token" ) func TestNextToken(t *testing.T) { tests := []struct { input string expected []struct { expectedType token.TokenType expectedLiteral string } }{ { input: "range(headers_content_length, 100, 300)", expected: []struct { expectedType token.TokenType expectedLiteral string }{ {token.IDENT, "range"}, {token.LPAREN, "("}, {token.IDENT, "headers_content_length"}, {token.COMMA, ","}, {token.INT, "100"}, {token.COMMA, ","}, {token.INT, "300"}, {token.RPAREN, ")"}, {token.EOF, ""}, }, }, { input: "in(status_code, [200, 201])", expected: []struct { expectedType token.TokenType expectedLiteral string }{ {token.IDENT, "in"}, {token.LPAREN, "("}, {token.IDENT, "status_code"}, {token.COMMA, ","}, {token.LBRACKET, "["}, {token.INT, "200"}, {token.COMMA, ","}, {token.INT, "201"}, {token.RBRACKET, "]"}, {token.RPAREN, ")"}, {token.EOF, ""}, }, }, { input: "not(in(status_code, [200, 201]))", expected: []struct { expectedType token.TokenType expectedLiteral string }{ {token.IDENT, "not"}, {token.LPAREN, "("}, {token.IDENT, "in"}, {token.LPAREN, "("}, {token.IDENT, "status_code"}, {token.COMMA, ","}, {token.LBRACKET, "["}, {token.INT, "200"}, {token.COMMA, ","}, {token.INT, "201"}, {token.RBRACKET, "]"}, {token.RPAREN, ")"}, {token.RPAREN, ")"}, {token.EOF, ""}, }, }, { input: "in(headers_Content_Type, [\"application/json\", \"application/xml\"])", expected: []struct { expectedType token.TokenType expectedLiteral string }{ {token.IDENT, "in"}, {token.LPAREN, "("}, {token.IDENT, "headers_Content_Type"}, {token.COMMA, ","}, {token.LBRACKET, "["}, {token.STRING, "application/json"}, {token.COMMA, ","}, {token.STRING, "application/xml"}, {token.RBRACKET, "]"}, {token.RPAREN, ")"}, {token.EOF, ""}, }, }, { input: "equals(body, '{\"c\": \"d\"}')", expected: []struct { expectedType token.TokenType expectedLiteral string }{ {token.IDENT, "equals"}, {token.LPAREN, "("}, {token.IDENT, "body"}, {token.COMMA, ","}, {token.STRING, "{\"c\": \"d\"}"}, {token.RPAREN, ")"}, {token.EOF, ""}, }, }, { input: "equals(json_path(employees.percentage), 32.3)", expected: []struct { expectedType token.TokenType expectedLiteral string }{ {token.IDENT, "equals"}, {token.LPAREN, "("}, {token.IDENT, "json_path"}, {token.LPAREN, "("}, {token.IDENT, "employees.percentage"}, {token.RPAREN, ")"}, {token.COMMA, ","}, {token.FLOAT, "32.3"}, {token.RPAREN, ")"}, {token.EOF, ""}, }, }, { input: "2+5-3*10/2<>", expected: []struct { expectedType token.TokenType expectedLiteral string }{ {token.INT, "2"}, {token.PLUS, "+"}, {token.INT, "5"}, {token.MINUS, "-"}, {token.INT, "3"}, {token.ASTERISK, "*"}, {token.INT, "10"}, {token.SLASH, "/"}, {token.INT, "2"}, {token.LT, "<"}, {token.GT, ">"}, {token.EOF, ""}, }, }, { input: "response_size == 234", expected: []struct { expectedType token.TokenType expectedLiteral string }{ {token.IDENT, "response_size"}, {token.EQ, "=="}, {token.INT, "234"}, {token.EOF, ""}, }, }, { input: "response_size != 234", expected: []struct { expectedType token.TokenType expectedLiteral string }{ {token.IDENT, "response_size"}, {token.NOT_EQ, "!="}, {token.INT, "234"}, {token.EOF, ""}, }, }, { input: "!exists(headers.referrer)", expected: []struct { expectedType token.TokenType expectedLiteral string }{ {token.BANG, "!"}, {token.IDENT, "exists"}, {token.LPAREN, "("}, {token.IDENT, "headers.referrer"}, {token.RPAREN, ")"}, {token.EOF, ""}, }, }, { input: "a = 5", expected: []struct { expectedType token.TokenType expectedLiteral string }{ {token.IDENT, "a"}, {token.ILLEGAL, "="}, {token.INT, "5"}, {token.EOF, ""}, }, }, { input: "60.1 $ 60.1", expected: []struct { expectedType token.TokenType expectedLiteral string }{ {token.FLOAT, "60.1"}, {token.ILLEGAL, "$"}, {token.FLOAT, "60.1"}, {token.EOF, ""}, }, }, { input: "%", expected: []struct { expectedType token.TokenType expectedLiteral string }{ {token.ILLEGAL, "%"}, {token.EOF, ""}, }, }, { input: "a =", expected: []struct { expectedType token.TokenType expectedLiteral string }{ {token.IDENT, "a"}, {token.ILLEGAL, "="}, {token.EOF, ""}, }, }, { input: "not(true) && not(false)", expected: []struct { expectedType token.TokenType expectedLiteral string }{ {token.IDENT, "not"}, {token.LPAREN, "("}, {token.TRUE, "true"}, {token.RPAREN, ")"}, {token.AND, "&&"}, {token.IDENT, "not"}, {token.LPAREN, "("}, {token.FALSE, "false"}, {token.RPAREN, ")"}, {token.EOF, ""}, }, }, { input: "equals(status_code,200) || equals(status_code,201)", expected: []struct { expectedType token.TokenType expectedLiteral string }{ {token.IDENT, "equals"}, {token.LPAREN, "("}, {token.IDENT, "status_code"}, {token.COMMA, ","}, {token.INT, "200"}, {token.RPAREN, ")"}, {token.OR, "||"}, {token.IDENT, "equals"}, {token.LPAREN, "("}, {token.IDENT, "status_code"}, {token.COMMA, ","}, {token.INT, "201"}, {token.RPAREN, ")"}, {token.EOF, ""}, }, }, { input: "equals(json_path(),null)", expected: []struct { expectedType token.TokenType expectedLiteral string }{ {token.IDENT, "equals"}, {token.LPAREN, "("}, {token.IDENT, "json_path"}, {token.LPAREN, "("}, {token.RPAREN, ")"}, {token.COMMA, ","}, {token.NULL, "null"}, {token.RPAREN, ")"}, {token.EOF, ""}, }, }, } for _, tt := range tests { l := New(tt.input) var tok token.Token i := 0 for tok = l.NextToken(); tok.Type != token.EOF; tok = l.NextToken() { if tok.Type != tt.expected[i].expectedType { t.Fatalf("tests[%d] - tokentype wrong. expected=%q, got=%q", i, tt.expected[i].expectedType, tok.Type) } if tok.Literal != tt.expected[i].expectedLiteral { t.Fatalf("tests[%d] - literal wrong. expected=%q, got=%q", i, tt.expected[i].expectedLiteral, tok.Literal) } i++ } } } ================================================ FILE: ddosify_engine/core/scenario/scripting/assertion/parser/parser.go ================================================ package parser import ( "fmt" "strconv" "go.ddosify.com/ddosify/core/scenario/scripting/assertion/ast" "go.ddosify.com/ddosify/core/scenario/scripting/assertion/lexer" "go.ddosify.com/ddosify/core/scenario/scripting/assertion/token" ) // precedences const ( _ int = iota LOWEST ANDOR // && || EQUALS // == LESSGREATER // > or < SUM // + PRODUCT // * PREFIX // -X or !X ARRAYDEFINE // [] OBJECTDEFINE // {} CALL // myFunction(X) ) var precedences = map[token.TokenType]int{ token.EQ: EQUALS, token.NOT_EQ: EQUALS, token.LT: LESSGREATER, token.GT: LESSGREATER, token.PLUS: SUM, token.MINUS: SUM, token.SLASH: PRODUCT, token.ASTERISK: PRODUCT, token.LBRACKET: ARRAYDEFINE, token.LBRACE: OBJECTDEFINE, token.LPAREN: CALL, token.AND: ANDOR, token.OR: ANDOR, } type ( prefixParseFn func() ast.Expression infixParseFn func(ast.Expression) ast.Expression ) type Parser struct { l *lexer.Lexer errors []string curToken token.Token peekToken token.Token prefixParseFns map[token.TokenType]prefixParseFn infixParseFns map[token.TokenType]infixParseFn } func New(l *lexer.Lexer) *Parser { p := &Parser{ l: l, errors: []string{}, } p.prefixParseFns = make(map[token.TokenType]prefixParseFn) p.prefixParseFns[token.IDENT] = p.parseIdentifier p.prefixParseFns[token.INT] = p.parseIntegerLiteral p.prefixParseFns[token.FLOAT] = p.parseFloatLiteral p.prefixParseFns[token.STRING] = p.parseStringLiteral p.prefixParseFns[token.BANG] = p.parsePrefixExpression p.prefixParseFns[token.MINUS] = p.parsePrefixExpression p.prefixParseFns[token.TRUE] = p.parseBoolean p.prefixParseFns[token.FALSE] = p.parseBoolean p.prefixParseFns[token.NULL] = p.parseNull p.prefixParseFns[token.LPAREN] = p.parseGroupedExpression p.prefixParseFns[token.LBRACKET] = p.parseArrayLiteral p.prefixParseFns[token.LBRACE] = p.parseObjectLiteral p.infixParseFns = make(map[token.TokenType]infixParseFn) p.infixParseFns[token.PLUS] = p.parseInfixExpression p.infixParseFns[token.MINUS] = p.parseInfixExpression p.infixParseFns[token.SLASH] = p.parseInfixExpression p.infixParseFns[token.ASTERISK] = p.parseInfixExpression p.infixParseFns[token.EQ] = p.parseInfixExpression p.infixParseFns[token.NOT_EQ] = p.parseInfixExpression p.infixParseFns[token.LT] = p.parseInfixExpression p.infixParseFns[token.GT] = p.parseInfixExpression p.infixParseFns[token.AND] = p.parseInfixExpression p.infixParseFns[token.OR] = p.parseInfixExpression p.infixParseFns[token.LPAREN] = p.parseCallExpression p.nextToken() p.nextToken() return p } func (p *Parser) nextToken() { p.curToken = p.peekToken p.peekToken = p.l.NextToken() } func (p *Parser) curTokenIs(t token.TokenType) bool { return p.curToken.Type == t } func (p *Parser) peekTokenIs(t token.TokenType) bool { return p.peekToken.Type == t } func (p *Parser) expectPeek(t token.TokenType) bool { if p.peekTokenIs(t) { p.nextToken() return true } else { p.peekError(t) return false } } func (p *Parser) Errors() []string { return p.errors } func (p *Parser) peekError(t token.TokenType) { msg := fmt.Sprintf("expected next token to be %s, got %s instead", t, p.peekToken.Type) p.errors = append(p.errors, msg) } func (p *Parser) noPrefixParseFnError(t token.TokenType) { msg := fmt.Sprintf("no prefix parse function for %s found", t) p.errors = append(p.errors, msg) } func (p *Parser) ParseExpressionStatement() *ast.ExpressionStatement { stmt := &ast.ExpressionStatement{Token: p.curToken} stmt.Expression = p.parseExpression(LOWEST) return stmt } func (p *Parser) parseExpression(precedence int) ast.Expression { prefix := p.prefixParseFns[p.curToken.Type] if prefix == nil { p.noPrefixParseFnError(p.curToken.Type) return nil } leftExp := prefix() if p.peekToken.Type == token.ILLEGAL { p.errors = append(p.errors, fmt.Sprintf("%s character is illegal", p.peekToken.Literal)) } // right side suck in, already parsed piece becomes leftExp for infix op for precedence < p.peekPrecedence() { infix := p.infixParseFns[p.peekToken.Type] if infix == nil { return leftExp } // suck in the left part that has been parsed so far p.nextToken() leftExp = infix(leftExp) } return leftExp } func (p *Parser) peekPrecedence() int { if pre, ok := precedences[p.peekToken.Type]; ok { return pre } return LOWEST } func (p *Parser) curPrecedence() int { if p, ok := precedences[p.curToken.Type]; ok { return p } return LOWEST } func (p *Parser) parseIdentifier() ast.Expression { return &ast.Identifier{Token: p.curToken, Value: p.curToken.Literal} } func (p *Parser) parseIntegerLiteral() ast.Expression { lit := &ast.IntegerLiteral{Token: p.curToken} value, err := strconv.ParseInt(p.curToken.Literal, 0, 64) if err != nil { msg := fmt.Sprintf("could not parse %q as integer", p.curToken.Literal) p.errors = append(p.errors, msg) return nil } lit.Value = value return lit } func (p *Parser) parseFloatLiteral() ast.Expression { lit := &ast.FloatLiteral{Token: p.curToken} value, err := strconv.ParseFloat(p.curToken.Literal, 64) if err != nil { msg := fmt.Sprintf("could not parse %q as integer", p.curToken.Literal) p.errors = append(p.errors, msg) return nil } lit.Value = value return lit } func (p *Parser) parseStringLiteral() ast.Expression { lit := &ast.StringLiteral{Token: p.curToken} lit.Value = p.curToken.Literal return lit } func (p *Parser) parsePrefixExpression() ast.Expression { expression := &ast.PrefixExpression{ Token: p.curToken, Operator: p.curToken.Literal, } p.nextToken() expression.Right = p.parseExpression(PREFIX) return expression } func (p *Parser) parseInfixExpression(left ast.Expression) ast.Expression { expression := &ast.InfixExpression{ Token: p.curToken, Operator: p.curToken.Literal, Left: left, } precedence := p.curPrecedence() p.nextToken() expression.Right = p.parseExpression(precedence) return expression } func (p *Parser) parseBoolean() ast.Expression { return &ast.Boolean{Token: p.curToken, Value: p.curTokenIs(token.TRUE)} } func (p *Parser) parseNull() ast.Expression { return &ast.NullLiteral{Token: p.curToken, Value: nil} } func (p *Parser) parseGroupedExpression() ast.Expression { p.nextToken() exp := p.parseExpression(LOWEST) if !p.expectPeek(token.RPAREN) { return nil } return exp } func (p *Parser) parseObjectLiteral() ast.Expression { lit := &ast.ObjectLiteral{Token: p.curToken} lit.Elems = p.parseObjectElements() return lit } func (p *Parser) parseArrayLiteral() ast.Expression { lit := &ast.ArrayLiteral{Token: p.curToken} lit.Elems = p.parseArrayElements() return lit } func (p *Parser) parseCallExpression(function ast.Expression) ast.Expression { exp := &ast.CallExpression{Token: p.curToken, Function: function} exp.Arguments = p.parseCallArguments() return exp } func (p *Parser) parseCallArguments() []ast.Expression { args := []ast.Expression{} if p.peekTokenIs(token.RPAREN) { p.nextToken() return args } p.nextToken() args = append(args, p.parseExpression(LOWEST)) for p.peekTokenIs(token.COMMA) { p.nextToken() p.nextToken() args = append(args, p.parseExpression(LOWEST)) } if !p.expectPeek(token.RPAREN) { return nil } return args } func (p *Parser) parseArrayElements() []ast.Expression { elems := []ast.Expression{} if p.peekTokenIs(token.RBRACKET) { p.nextToken() return elems } p.nextToken() elems = append(elems, p.parseExpression(LOWEST)) for p.peekTokenIs(token.COMMA) { p.nextToken() p.nextToken() elems = append(elems, p.parseExpression(LOWEST)) } if !p.expectPeek(token.RBRACKET) { return nil } return elems } func (p *Parser) parseObjectElements() map[string]ast.Expression { elems := make(map[string]ast.Expression) if p.peekTokenIs(token.RBRACE) { p.nextToken() return elems } p.nextToken() key := p.parseExpression(LOWEST) if !p.expectPeek(token.COLON) { return nil } p.nextToken() value := p.parseExpression(LOWEST) elems[key.String()] = value for p.peekTokenIs(token.COMMA) { p.nextToken() p.nextToken() key := p.parseExpression(LOWEST) if !p.expectPeek(token.COLON) { return nil } p.nextToken() value := p.parseExpression(LOWEST) elems[key.String()] = value } if !p.expectPeek(token.RBRACE) { return nil } return elems } ================================================ FILE: ddosify_engine/core/scenario/scripting/assertion/parser/parser_test.go ================================================ package parser import ( "fmt" "testing" "go.ddosify.com/ddosify/core/scenario/scripting/assertion/ast" "go.ddosify.com/ddosify/core/scenario/scripting/assertion/lexer" ) func TestIdentifierExpression(t *testing.T) { input := "foobar" l := lexer.New(input) p := New(l) expressionStmt := p.ParseExpressionStatement() checkParserErrors(t, p) ident, ok := expressionStmt.Expression.(*ast.Identifier) if !ok { t.Fatalf("exp not *ast.Identifier. got=%T", expressionStmt.Expression) } if ident.Value != "foobar" { t.Errorf("ident.Value not %s. got=%s", "foobar", ident.Value) } if ident.TokenLiteral() != "foobar" { t.Errorf("ident.TokenLiteral not %s. got=%s", "foobar", ident.TokenLiteral()) } } func TestIntegerLiteralExpression(t *testing.T) { input := "5" l := lexer.New(input) p := New(l) expressionStmt := p.ParseExpressionStatement() checkParserErrors(t, p) literal, ok := expressionStmt.Expression.(*ast.IntegerLiteral) if !ok { t.Fatalf("exp not *ast.IntegerLiteral. got=%T", expressionStmt.Expression) } if literal.Value != 5 { t.Errorf("literal.Value not %d. got=%d", 5, literal.Value) } } func TestArrayLiteralExpression(t *testing.T) { input := "[x,10,\"xyz\",[243,55]]" l := lexer.New(input) p := New(l) expressionStmt := p.ParseExpressionStatement() checkParserErrors(t, p) literal, ok := expressionStmt.Expression.(*ast.ArrayLiteral) if !ok { t.Fatalf("exp not *ast.ArrayLiteral. got=%T", expressionStmt.Expression) } if len(literal.Elems) != 4 { t.Errorf("len(literal.Elems) not %d. got=%d", 4, len(literal.Elems)) } // identifier if literal.Elems[0].TokenLiteral() != "x" { t.Errorf("literal.TokenLiteral[0] not %s. got=%s", "x", literal.Elems[0].TokenLiteral()) } // integerLiteral if literal.Elems[1].(interface{ GetVal() interface{} }).GetVal() != int64(10) { t.Errorf("literal.TokenLiteral not %s. got=%s", "5", literal.TokenLiteral()) } // stringLiteral if literal.Elems[2].(interface{ GetVal() interface{} }).GetVal() != "xyz" { t.Errorf("literal.TokenLiteral not %s. got=%s", "5", literal.TokenLiteral()) } // arrayLiteral if literal.Elems[3].(*ast.ArrayLiteral).Elems[0].(interface{ GetVal() interface{} }).GetVal() != int64(243) { t.Errorf("literal.TokenLiteral not %s. got=%s", "5", literal.TokenLiteral()) } if literal.Elems[3].(*ast.ArrayLiteral).Elems[1].(interface{ GetVal() interface{} }).GetVal() != int64(55) { t.Errorf("literal.TokenLiteral not %s. got=%s", "5", literal.TokenLiteral()) } } func TestObjectLiteralExpression(t *testing.T) { // input := "[x,10,\"xyz\",[243,55]]" input := `{"name":"John", "age":30, "cars":[ "Honda", "Alfa", "Opel" ]}` l := lexer.New(input) p := New(l) expressionStmt := p.ParseExpressionStatement() checkParserErrors(t, p) literal, ok := expressionStmt.Expression.(*ast.ObjectLiteral) if !ok { t.Fatalf("exp not *ast.ObjectLiteral. got=%T", expressionStmt.Expression) } if len(literal.Elems) != 3 { t.Errorf("len(literal.Elems) not %d. got=%d", 4, len(literal.Elems)) } // identifier if literal.Elems["name"].TokenLiteral() != "John" { t.Errorf("literal.TokenLiteral[name] not %s. got=%s", "x", literal.Elems["name"].TokenLiteral()) } if literal.Elems["age"].TokenLiteral() != "30" { t.Errorf("literal.TokenLiteral[age] not %s. got=%s", "x", literal.Elems["age"].TokenLiteral()) } array := literal.Elems["cars"].(*ast.ArrayLiteral) if array.Elems[0].String() != "Honda" { t.Errorf("literal.TokenLiteral[0] not %s. got=%s", "x", array.Elems[0].TokenLiteral()) } if array.Elems[1].String() != "Alfa" { t.Errorf("literal.TokenLiteral[age] not %s. got=%s", "x", array.Elems[1].TokenLiteral()) } if array.Elems[2].String() != "Opel" { t.Errorf("literal.TokenLiteral[age] not %s. got=%s", "x", array.Elems[2].TokenLiteral()) } } func TestFloatLiteralExpression(t *testing.T) { input := "5.2" l := lexer.New(input) p := New(l) expressionStmt := p.ParseExpressionStatement() checkParserErrors(t, p) literal, ok := expressionStmt.Expression.(*ast.FloatLiteral) if !ok { t.Fatalf("exp not *ast.IntegerLiteral. got=%T", expressionStmt.Expression) } if literal.Value != 5.2 { t.Errorf("literal.Value not %f. got=%f", 5.2, literal.Value) } } func TestExpectPeek(t *testing.T) { input := "[22,)" l := lexer.New(input) p := New(l) _ = p.ParseExpressionStatement() errors := p.Errors() if len(errors) == 0 { t.Error("parser should have given error") } } func TestParsingPrefixExpressions(t *testing.T) { prefixTests := []struct { input string operator string value interface{} }{ {"!5", "!", 5}, {"-15", "-", 15}, {"!foobar", "!", "foobar"}, {"-foobar", "-", "foobar"}, {"!true", "!", true}, {"!false", "!", false}, } for _, tt := range prefixTests { l := lexer.New(tt.input) p := New(l) expressionStmt := p.ParseExpressionStatement() checkParserErrors(t, p) exp, ok := expressionStmt.Expression.(*ast.PrefixExpression) if !ok { t.Fatalf("stmt is not ast.PrefixExpression. got=%T", expressionStmt.Expression) } if exp.Operator != tt.operator { t.Fatalf("exp.Operator is not '%s'. got=%s", tt.operator, exp.Operator) } if !testLiteralExpression(t, exp.Right, tt.value) { return } } } func TestParsingInfixExpressions(t *testing.T) { infixTests := []struct { input string leftValue interface{} operator string rightValue interface{} }{ {"5 + 5", 5, "+", 5}, {"5 - 5", 5, "-", 5}, {"5 * 5", 5, "*", 5}, {"5 / 5", 5, "/", 5}, {"5 > 5", 5, ">", 5}, {"5 < 5", 5, "<", 5}, {"5 == 5", 5, "==", 5}, {"5 != 5", 5, "!=", 5}, {"foobar + barfoo", "foobar", "+", "barfoo"}, {"foobar - barfoo", "foobar", "-", "barfoo"}, {"foobar * barfoo", "foobar", "*", "barfoo"}, {"foobar / barfoo", "foobar", "/", "barfoo"}, {"foobar > barfoo", "foobar", ">", "barfoo"}, {"foobar < barfoo", "foobar", "<", "barfoo"}, {"foobar == barfoo", "foobar", "==", "barfoo"}, {"foobar != barfoo", "foobar", "!=", "barfoo"}, {"true == true", true, "==", true}, {"true != false", true, "!=", false}, {"false == false", false, "==", false}, {"true && false", true, "&&", false}, {"true || false", true, "||", false}, } for _, tt := range infixTests { l := lexer.New(tt.input) p := New(l) expressionStmt := p.ParseExpressionStatement() checkParserErrors(t, p) if !testInfixExpression(t, expressionStmt.Expression, tt.leftValue, tt.operator, tt.rightValue) { return } } } func TestOperatorPrecedenceParsing(t *testing.T) { tests := []struct { input string expected string }{ { "-a * b", "((-a) * b)", }, { "!-a", "(!(-a))", }, { "a + b + c", "((a + b) + c)", }, { "a + b - c", "((a + b) - c)", }, { "a * b * c", "((a * b) * c)", }, { "a * b / c", "((a * b) / c)", }, { "a + b / c", "(a + (b / c))", }, { "a + b * c + d / e - f", "(((a + (b * c)) + (d / e)) - f)", }, { "5 > 4 == 3 < 4", "((5 > 4) == (3 < 4))", }, { "5 < 4 != 3 > 4", "((5 < 4) != (3 > 4))", }, { "3 + 4 * 5 == 3 * 1 + 4 * 5", "((3 + (4 * 5)) == ((3 * 1) + (4 * 5)))", }, { "true", "true", }, { "false", "false", }, { "3 > 5 == false", "((3 > 5) == false)", }, { "3 < 5 == true", "((3 < 5) == true)", }, { "1 + (2 + 3) + 4", "((1 + (2 + 3)) + 4)", }, { "(5 + 5) * 2", "((5 + 5) * 2)", }, { "2 / (5 + 5)", "(2 / (5 + 5))", }, { "(5 + 5) * 2 * (5 + 5)", "(((5 + 5) * 2) * (5 + 5))", }, { "-(5 + 5)", "(-(5 + 5))", }, { "!(true == true)", "(!(true == true))", }, { "a + add(b * c) + d", "((a + add((b * c))) + d)", }, { "add(a, b, 1, 2 * 3, 4 + 5, add(6, 7 * 8))", "add(a,b,1,(2 * 3),(4 + 5),add(6,(7 * 8)))", }, { "add(a + b + c * d / f + g)", "add((((a + b) + ((c * d) / f)) + g))", }, } for _, tt := range tests { l := lexer.New(tt.input) p := New(l) expressionStmt := p.ParseExpressionStatement() checkParserErrors(t, p) actual := expressionStmt.String() if actual != tt.expected { t.Errorf("expected=%q, got=%q", tt.expected, actual) } } } func TestBooleanExpression(t *testing.T) { tests := []struct { input string expectedBoolean bool }{ {"true", true}, {"false", false}, } for _, tt := range tests { l := lexer.New(tt.input) p := New(l) expressionStmt := p.ParseExpressionStatement() checkParserErrors(t, p) boolean, ok := expressionStmt.Expression.(*ast.Boolean) if !ok { t.Fatalf("exp not *ast.Boolean. got=%T", expressionStmt.Expression) } if boolean.Value != tt.expectedBoolean { t.Errorf("boolean.Value not %t. got=%t", tt.expectedBoolean, boolean.Value) } } } func TestCallExpressionParsing(t *testing.T) { input := "add(1, 2 * 3, 4 + 5)" l := lexer.New(input) p := New(l) expressionStmt := p.ParseExpressionStatement() checkParserErrors(t, p) exp, ok := expressionStmt.Expression.(*ast.CallExpression) if !ok { t.Fatalf("stmt.Expression is not ast.CallExpression. got=%T", expressionStmt.Expression) } if !testIdentifier(t, exp.Function, "add") { return } if len(exp.Arguments) != 3 { t.Fatalf("wrong length of arguments. got=%d", len(exp.Arguments)) } testLiteralExpression(t, exp.Arguments[0], 1) testInfixExpression(t, exp.Arguments[1], 2, "*", 3) testInfixExpression(t, exp.Arguments[2], 4, "+", 5) } func TestCallExpressionParameterParsing(t *testing.T) { tests := []struct { input string expectedIdent string expectedArgs []string }{ { input: "add()", expectedIdent: "add", expectedArgs: []string{}, }, { input: "add(1)", expectedIdent: "add", expectedArgs: []string{"1"}, }, { input: "add(1, 2 * 3, 4 + 5)", expectedIdent: "add", expectedArgs: []string{"1", "(2 * 3)", "(4 + 5)"}, }, } for _, tt := range tests { l := lexer.New(tt.input) p := New(l) expressionStmt := p.ParseExpressionStatement() checkParserErrors(t, p) exp, ok := expressionStmt.Expression.(*ast.CallExpression) if !ok { t.Fatalf("stmt.Expression is not ast.CallExpression. got=%T", expressionStmt.Expression) } if !testIdentifier(t, exp.Function, tt.expectedIdent) { return } if len(exp.Arguments) != len(tt.expectedArgs) { t.Fatalf("wrong number of arguments. want=%d, got=%d", len(tt.expectedArgs), len(exp.Arguments)) } for i, arg := range tt.expectedArgs { if exp.Arguments[i].String() != arg { t.Errorf("argument %d wrong. want=%q, got=%q", i, arg, exp.Arguments[i].String()) } } } } func testInfixExpression(t *testing.T, exp ast.Expression, left interface{}, operator string, right interface{}) bool { opExp, ok := exp.(*ast.InfixExpression) if !ok { t.Errorf("exp is not ast.OperatorExpression. got=%T(%s)", exp, exp) return false } if !testLiteralExpression(t, opExp.Left, left) { return false } if opExp.Operator != operator { t.Errorf("exp.Operator is not '%s'. got=%q", operator, opExp.Operator) return false } if !testLiteralExpression(t, opExp.Right, right) { return false } return true } func testLiteralExpression( t *testing.T, exp ast.Expression, expected interface{}, ) bool { switch v := expected.(type) { case int: return testIntegerLiteral(t, exp, int64(v)) case int64: return testIntegerLiteral(t, exp, v) case string: return testIdentifier(t, exp, v) case bool: return testBooleanLiteral(t, exp, v) } t.Errorf("type of exp not handled. got=%T", exp) return false } func testIntegerLiteral(t *testing.T, il ast.Expression, value int64) bool { integ, ok := il.(*ast.IntegerLiteral) if !ok { t.Errorf("il not *ast.IntegerLiteral. got=%T", il) return false } if integ.Value != value { t.Errorf("integ.Value not %d. got=%d", value, integ.Value) return false } if integ.TokenLiteral() != fmt.Sprintf("%d", value) { t.Errorf("integ.TokenLiteral not %d. got=%s", value, integ.TokenLiteral()) return false } return true } func testIdentifier(t *testing.T, exp ast.Expression, value string) bool { ident, ok := exp.(*ast.Identifier) if !ok { t.Errorf("exp not *ast.Identifier. got=%T", exp) return false } if ident.Value != value { t.Errorf("ident.Value not %s. got=%s", value, ident.Value) return false } if ident.TokenLiteral() != value { t.Errorf("ident.TokenLiteral not %s. got=%s", value, ident.TokenLiteral()) return false } return true } func testBooleanLiteral(t *testing.T, exp ast.Expression, value bool) bool { bo, ok := exp.(*ast.Boolean) if !ok { t.Errorf("exp not *ast.Boolean. got=%T", exp) return false } if bo.Value != value { t.Errorf("bo.Value not %t. got=%t", value, bo.Value) return false } if bo.TokenLiteral() != fmt.Sprintf("%t", value) { t.Errorf("bo.TokenLiteral not %t. got=%s", value, bo.TokenLiteral()) return false } return true } func checkParserErrors(t *testing.T, p *Parser) { errors := p.Errors() if len(errors) == 0 { return } t.Errorf("parser has %d errors", len(errors)) for _, msg := range errors { t.Errorf("parser error: %q", msg) } t.FailNow() } ================================================ FILE: ddosify_engine/core/scenario/scripting/assertion/test_files/a.txt ================================================ abc ================================================ FILE: ddosify_engine/core/scenario/scripting/assertion/test_files/currencies.json ================================================ [ "AED", "ARS", "AUD", "BGN", "BHD", "BRL", "CAD", "CHF", "CNY", "DKK", "DZD", "EUR", "FKP", "INR", "JEP", "JPY", "KES", "KWD", "KZT", "MXN", "NZD", "RUB", "SEK", "SGD", "TRY", "USD" ] ================================================ FILE: ddosify_engine/core/scenario/scripting/assertion/test_files/jsonArray.json ================================================ ["xyz","abc"] ================================================ FILE: ddosify_engine/core/scenario/scripting/assertion/test_files/jsonMap.json ================================================ { "ask": 130.75, "askSize": 10, "averageAnalystRating": "2.0 - Buy" } ================================================ FILE: ddosify_engine/core/scenario/scripting/assertion/test_files/number.json ================================================ 5 ================================================ FILE: ddosify_engine/core/scenario/scripting/assertion/token/token.go ================================================ package token type TokenType string type Token struct { Type TokenType Literal string } const ( ILLEGAL = "ILLEGAL" EOF = "EOF" // Identifiers + literals IDENT = "IDENT" // not, equals, json_path, contains, range... INT = "INT" // 200, 201 FLOAT = "FLOAT" // 10.5 STRING = "STRING" // Content-Type // Operators PLUS = "+" MINUS = "-" BANG = "!" ASTERISK = "*" SLASH = "/" AND = "&&" OR = "||" LT = "<" GT = ">" EQ = "==" NOT_EQ = "!=" // Delimiters COMMA = "," LPAREN = "(" RPAREN = ")" LBRACE = "{" RBRACE = "}" LBRACKET = "[" RBRACKET = "]" COLON = ":" // Keywords TRUE = "TRUE" FALSE = "FALSE" NULL = "NULL" ) var keywords = map[string]TokenType{ "true": TRUE, "false": FALSE, "null": NULL, } func LookupIdent(ident string) TokenType { if tok, ok := keywords[ident]; ok { return tok } return IDENT } ================================================ FILE: ddosify_engine/core/scenario/scripting/extraction/base.go ================================================ package extraction import ( "errors" "fmt" "net/http" "go.ddosify.com/ddosify/core/types" ) func Extract(source interface{}, ce types.EnvCaptureConf) (val interface{}, err error) { defer func() { if r := recover(); r != nil { switch x := r.(type) { case string: err = errors.New(x) case error: err = x default: err = errors.New("Unknown panic") } val = nil } }() if source == nil { return "", ExtractionError{ msg: "source is nil", } } switch ce.From { case types.Header: header := source.(http.Header) if ce.Key != nil { // key specified val = header.Get(*ce.Key) if val == "" { err = fmt.Errorf("http header %s not found", *ce.Key) } else if ce.RegExp != nil { // run regex for found value val, err = ExtractWithRegex(val, *ce.RegExp) } } else { err = fmt.Errorf("http header key not specified") } case types.Body: if ce.JsonPath != nil { val, err = ExtractFromJson(source, *ce.JsonPath) } else if ce.RegExp != nil { val, err = ExtractWithRegex(source, *ce.RegExp) } else if ce.Xpath != nil { val, err = ExtractFromXml(source, *ce.Xpath) } else if ce.XpathHtml != nil { val, err = ExtractFromHtml(source, *ce.XpathHtml) } case types.Cookie: cookies := source.(map[string]*http.Cookie) if ce.CookieName != nil { // cookie name specified c, ok := cookies[*ce.CookieName] if !ok { err = fmt.Errorf("cookie %s not found", *ce.CookieName) } else { val = c.Value } } else { err = fmt.Errorf("cookie name not specified") } } if err != nil { return "", ExtractionError{ msg: fmt.Sprintf("%v", err), wrappedErr: err, } } return val, nil } func ExtractWithRegex(source interface{}, regexConf types.RegexCaptureConf) (val interface{}, err error) { re := regexExtractor{} re.Init(*regexConf.Exp) switch s := source.(type) { case []byte: // from response body return re.extractFromByteSlice(s, regexConf.No) case string: // from response header return re.extractFromString(s, regexConf.No) default: return "", fmt.Errorf("Unsupported type for extraction source") } } func ExtractFromJson(source interface{}, jsonPath string) (interface{}, error) { je := jsonExtractor{} switch s := source.(type) { case []byte: // from response body return je.extractFromByteSlice(s, jsonPath) case string: // from response header return je.extractFromString(s, jsonPath) default: return "", fmt.Errorf("Unsupported type for extraction source") } } func ExtractFromXml(source interface{}, xPath string) (interface{}, error) { xe := xmlExtractor{} switch s := source.(type) { case []byte: // from response body return xe.extractFromByteSlice(s, xPath) case string: // from response header return xe.extractFromString(s, xPath) default: return "", fmt.Errorf("Unsupported type for extraction source") } } func ExtractFromHtml(source interface{}, xPath string) (interface{}, error) { xe := htmlExtractor{} switch s := source.(type) { case []byte: // from response body return xe.extractFromByteSlice(s, xPath) case string: // from response header return xe.extractFromString(s, xPath) default: return "", fmt.Errorf("Unsupported type for extraction source") } } type ExtractionError struct { // UnWrappable msg string wrappedErr error } func (sc ExtractionError) Error() string { return sc.msg } func (sc ExtractionError) Unwrap() error { return sc.wrappedErr } ================================================ FILE: ddosify_engine/core/scenario/scripting/extraction/base_test.go ================================================ package extraction import ( "errors" "net/http" "runtime" "testing" "go.ddosify.com/ddosify/core/types" ) func TestHttpHeaderKey_NotSpecified(t *testing.T) { ce := types.EnvCaptureConf{ JsonPath: nil, Xpath: nil, RegExp: &types.RegexCaptureConf{}, Name: "", From: types.Header, Key: nil, } _, err := Extract(http.Header{}, ce) if err == nil { t.Errorf("Expected error when header key not specified") } } func TestExtract_TypeAssertErrorRecover(t *testing.T) { headerKey := "x" ce := types.EnvCaptureConf{ JsonPath: nil, Xpath: nil, RegExp: nil, Name: "", From: types.Header, Key: &headerKey, } // source should be http.Header _, err := Extract("sdfds", ce) var assertError *runtime.TypeAssertionError if !errors.As(err, &assertError) { t.Errorf("Expected error must be TypeAssertionError, got %v", err) } } func TestExtract_NilSource(t *testing.T) { headerKey := "x" ce := types.EnvCaptureConf{ JsonPath: nil, Xpath: nil, RegExp: nil, Name: "", From: types.Header, Key: &headerKey, } _, err := Extract(nil, ce) if err == nil { t.Errorf("error expected, got nil") } } func TestExtract_InvalidXml(t *testing.T) { xpath := "" ce := types.EnvCaptureConf{ JsonPath: nil, Xpath: &xpath, RegExp: nil, Name: "", From: types.Body, Key: nil, } _, err := Extract([]byte("xxx"), ce) if err == nil { t.Errorf("error expected, got nil") } } func TestCookieName_NotSpecified(t *testing.T) { ce := types.EnvCaptureConf{ JsonPath: nil, Xpath: nil, RegExp: &types.RegexCaptureConf{}, Name: "", From: types.Cookie, Key: nil, CookieName: nil, } _, err := Extract(map[string]*http.Cookie{}, ce) if err == nil { t.Errorf("Expected error when cookie key not specified") } } ================================================ FILE: ddosify_engine/core/scenario/scripting/extraction/html.go ================================================ package extraction import ( "bytes" "fmt" "github.com/antchfx/htmlquery" ) type htmlExtractor struct { } func (xe htmlExtractor) extractFromByteSlice(source []byte, xPath string) (interface{}, error) { reader := bytes.NewBuffer(source) rootNode, err := htmlquery.Parse(reader) if err != nil { return nil, err } // returns the first matched element foundNode, err := htmlquery.Query(rootNode, xPath) if foundNode == nil || err != nil { return nil, fmt.Errorf("no match for the xPath_html: %s", xPath) } return foundNode.FirstChild.Data, nil } func (xe htmlExtractor) extractFromString(source string, xPath string) (interface{}, error) { reader := bytes.NewBufferString(source) rootNode, err := htmlquery.Parse(reader) if err != nil { return nil, err } // returns the first matched element foundNode, err := htmlquery.Query(rootNode, xPath) if foundNode == nil || err != nil { return nil, fmt.Errorf("no match for this xpath_html") } return foundNode.FirstChild.Data, nil } ================================================ FILE: ddosify_engine/core/scenario/scripting/extraction/html_test.go ================================================ package extraction import ( "fmt" "strings" "testing" ) func TestHtmlExtraction(t *testing.T) { expected := "Html Title" HtmlSource := fmt.Sprintf(`

%s

My first paragraph.

`, expected) xe := htmlExtractor{} xpath := "//body/h1" val, err := xe.extractFromByteSlice([]byte(HtmlSource), xpath) if err != nil { t.Errorf("TestHtmlExtraction %v", err) } if !strings.EqualFold(val.(string), expected) { t.Errorf("TestHtmlExtraction expected: %s, got: %s", expected, val) } } func TestHtmlExtractionSeveralNode(t *testing.T) { //should extract only the first one expected := "Html Title" HtmlSource := fmt.Sprintf(`

%s

another node

My first paragraph.

`, expected) xe := htmlExtractor{} xpath := "//h1" val, err := xe.extractFromByteSlice([]byte(HtmlSource), xpath) if err != nil { t.Errorf("TestHtmlExtraction %v", err) } if !strings.EqualFold(val.(string), expected) { t.Errorf("TestHtmlExtraction expected: %s, got: %s", expected, val) } } func TestHtmlExtraction_PathNotFound(t *testing.T) { expected := "XML Title" xmlSource := fmt.Sprintf(`

%s

another node

My first paragraph.

`, expected) xe := htmlExtractor{} xpath := "//h2" _, err := xe.extractFromByteSlice([]byte(xmlSource), xpath) if err == nil { t.Errorf("TestHtmlExtraction_PathNotFound, should be err, got :%v", err) } } func TestInvalidHtml(t *testing.T) { xmlSource := `invalid html source` xe := htmlExtractor{} xpath := "//input" _, err := xe.extractFromByteSlice([]byte(xmlSource), xpath) if err == nil { t.Errorf("TestInvalidXml, should be err, got :%v", err) } } func TestHtmlComplexExtraction(t *testing.T) { expected := "Html Title" HtmlSource := fmt.Sprintf(`

%s

My first paragraph.

`, expected) xe := htmlExtractor{} xpath := "//body/h1" val, err := xe.extractFromByteSlice([]byte(HtmlSource), xpath) if err != nil { t.Errorf("TestHtmlExtraction %v", err) } if !strings.EqualFold(val.(string), expected) { t.Errorf("TestHtmlExtraction expected: %s, got: %s", expected, val) } } ================================================ FILE: ddosify_engine/core/scenario/scripting/extraction/json.go ================================================ package extraction import ( "encoding/json" "fmt" "strings" "github.com/tidwall/gjson" ) type jsonExtractor struct { } var unmarshalJsonCapture = func(result gjson.Result) (interface{}, error) { bRaw := []byte(result.Raw) if result.IsObject() { jObject := map[string]interface{}{} err := json.Unmarshal(bRaw, &jObject) if err == nil { return jObject, err } } if result.IsArray() { jInterfaceSlice := []interface{}{} err := json.Unmarshal(bRaw, &jInterfaceSlice) if err == nil { return jInterfaceSlice, err } } if result.IsBool() { jBool := false err := json.Unmarshal(bRaw, &jBool) if err == nil { return jBool, err } } return nil, fmt.Errorf("json could not be unmarshaled") } func (je jsonExtractor) extractFromString(source string, jsonPath string) (interface{}, error) { result := gjson.Get(source, jsonPath) // path not found if result.Raw == "" && result.Type == gjson.Null { return "", fmt.Errorf("no match for the json path: %s", jsonPath) } switch result.Type { case gjson.String: return result.String(), nil case gjson.Null: return nil, nil case gjson.False: return false, nil case gjson.Number: number := result.String() if strings.Contains(number, ".") { // float return result.Float(), nil } return result.Int(), nil case gjson.True: return true, nil case gjson.JSON: return unmarshalJsonCapture(result) default: return "", nil } } func (je jsonExtractor) extractFromByteSlice(source []byte, jsonPath string) (interface{}, error) { result := gjson.GetBytes(source, jsonPath) // path not found if result.Raw == "" && result.Type == gjson.Null { return "", fmt.Errorf("no match for the json path: %s", jsonPath) } switch result.Type { case gjson.String: return result.String(), nil case gjson.Null: return nil, nil case gjson.False: return false, nil case gjson.Number: number := result.String() if strings.Contains(number, ".") { // float return result.Float(), nil } return result.Int(), nil case gjson.True: return true, nil case gjson.JSON: return unmarshalJsonCapture(result) default: return "", nil } } ================================================ FILE: ddosify_engine/core/scenario/scripting/extraction/json_test.go ================================================ package extraction import ( "encoding/json" "reflect" "strings" "testing" ) func TestJsonExtract_String(t *testing.T) { payload := map[string]interface{}{ "name": map[string]interface{}{ "first": "Janet", "last": "Prichard", }, "age": 47, } byteSlice, _ := json.Marshal(payload) je := jsonExtractor{} val, _ := je.extractFromByteSlice(byteSlice, "name.last") if val != "Prichard" { t.Errorf("Json Extract Error") } val, _ = je.extractFromString(string(byteSlice), "name.last") if val != "Prichard" { t.Errorf("Json Extract Error") } } func TestJsonExtract_Object(t *testing.T) { expected := map[string]interface{}{ "first": "Janet", "last": "Prichard", } payload := map[string]interface{}{ "name": expected, "age": 47, } byteSlice, _ := json.Marshal(payload) je := jsonExtractor{} val, _ := je.extractFromByteSlice(byteSlice, "name") if !reflect.DeepEqual(val, expected) { t.Errorf("TestJsonExtract_Object failed, expected %#v, found %#v", expected, val) } val, _ = je.extractFromString(string(byteSlice), "name") if !reflect.DeepEqual(val, expected) { t.Errorf("TestJsonExtract_Object failed, expected %#v, found %#v", expected, val) } } func TestJsonExtract_Float(t *testing.T) { var expected float64 = 52.2 payload := map[string]interface{}{ "age": expected, } byteSlice, _ := json.Marshal(payload) je := jsonExtractor{} val, _ := je.extractFromByteSlice(byteSlice, "age") val2 := val.(float64) // json number -> float64 if !reflect.DeepEqual(val2, expected) { t.Errorf("TestJsonExtract_Float failed, expected %#v, found %#v", expected, val) } val, _ = je.extractFromString(string(byteSlice), "age") val22 := val.(float64) // json number -> float64 if !reflect.DeepEqual(val22, expected) { t.Errorf("TestJsonExtract_Float failed, expected %#v, found %#v", expected, val) } } func TestJsonExtract_Int(t *testing.T) { var expected int = 52 payload := map[string]interface{}{ "age": expected, } byteSlice, _ := json.Marshal(payload) je := jsonExtractor{} val, _ := je.extractFromByteSlice(byteSlice, "age") val2 := val.(int64) // json number -> float64 if !reflect.DeepEqual(int(val2), expected) { t.Errorf("TestJsonExtract_Int failed, expected %#v, found %#v", expected, val) } val, _ = je.extractFromString(string(byteSlice), "age") val22 := val.(int64) // json number -> float64 if !reflect.DeepEqual(int(val22), expected) { t.Errorf("TestJsonExtract_Int failed, expected %#v, found %#v", expected, val) } } func TestJsonExtract_Nil(t *testing.T) { payload := map[string]interface{}{ "age": nil, } byteSlice, _ := json.Marshal(payload) je := jsonExtractor{} val, _ := je.extractFromByteSlice(byteSlice, "age") if !reflect.DeepEqual(val, nil) { t.Errorf("TestJsonExtract_Nil failed, expected %#v, found %#v", nil, val) } val, _ = je.extractFromString(string(byteSlice), "age") if !reflect.DeepEqual(val, nil) { t.Errorf("TestJsonExtract_Nil failed, expected %#v, found %#v", nil, val) } } func TestJsonExtract_Bool(t *testing.T) { je := jsonExtractor{} expected := true expected1 := false payload := map[string]interface{}{ "age": expected, "age1": expected1, } byteSlice, _ := json.Marshal(payload) val, _ := je.extractFromByteSlice(byteSlice, "age") val1, _ := je.extractFromByteSlice(byteSlice, "age1") if !reflect.DeepEqual(val, expected) || !reflect.DeepEqual(val1, expected1) { t.Errorf("TestJsonExtract_Bool failed, expected %#v, found %#v", expected, val) } expected = false payload = map[string]interface{}{ "age": expected, } byteSlice, _ = json.Marshal(payload) val, _ = je.extractFromString(string(byteSlice), "age") if !reflect.DeepEqual(val, expected) { t.Errorf("TestJsonExtract_Bool failed, expected %#v, found %#v", expected, val) } } func TestJsonExtract_JsonArray(t *testing.T) { t.SkipNow() expected := []string{"a", "b"} payload := map[string]interface{}{ "age": expected, } byteSlice, _ := json.Marshal(payload) je := jsonExtractor{} val, _ := je.extractFromByteSlice(byteSlice, "age") if !reflect.DeepEqual(val, expected) { t.Errorf("TestJsonExtract_JsonArray failed, expected %#v, found %#v", expected, val) } val, _ = je.extractFromString(string(byteSlice), "age") if !reflect.DeepEqual(val, expected) { t.Errorf("TestJsonExtract_JsonArray failed, expected %#v, found %#v", expected, val) } } func TestJsonExtract_JsonIntArray(t *testing.T) { t.SkipNow() expected := []int{2, 4} payload := map[string]interface{}{ "age": expected, } byteSlice, _ := json.Marshal(payload) je := jsonExtractor{} val, _ := je.extractFromByteSlice(byteSlice, "age") expectedFloat := []float64{2, 4} if !reflect.DeepEqual(val, expectedFloat) { t.Errorf("TestJsonExtract_JsonIntArray failed, expected %#v, found %#v", expected, val) } val, _ = je.extractFromString(string(byteSlice), "age") if !reflect.DeepEqual(val, expectedFloat) { t.Errorf("TestJsonExtract_JsonIntArray failed, expected %#v, found %#v", expected, val) } } func TestJsonExtract_JsonFloatArray(t *testing.T) { t.SkipNow() expected := []float64{2.33, 4.55} payload := map[string]interface{}{ "age": expected, } byteSlice, _ := json.Marshal(payload) je := jsonExtractor{} val, _ := je.extractFromByteSlice(byteSlice, "age") if !reflect.DeepEqual(val, expected) { t.Errorf("TestJsonExtract_JsonFloatArray failed, expected %#v, found %#v", expected, val) } val, _ = je.extractFromString(string(byteSlice), "age") if !reflect.DeepEqual(val, expected) { t.Errorf("TestJsonExtract_JsonFloatArray failed, expected %#v, found %#v", expected, val) } } func TestJsonExtract_JsonBoolArray(t *testing.T) { t.SkipNow() expected := []bool{true, false} payload := map[string]interface{}{ "age": expected, } byteSlice, _ := json.Marshal(payload) je := jsonExtractor{} val, _ := je.extractFromByteSlice(byteSlice, "age") if !reflect.DeepEqual(val, expected) { t.Errorf("TestJsonExtract_JsonBoolArray failed, expected %#v, found %#v", expected, val) } val, _ = je.extractFromString(string(byteSlice), "age") if !reflect.DeepEqual(val, expected) { t.Errorf("TestJsonExtract_JsonBoolArray failed, expected %#v, found %#v", expected, val) } } func TestJsonExtract_ObjectArray(t *testing.T) { t.SkipNow() expected := []map[string]interface{}{ {"x": "cc"}, } payload := map[string]interface{}{ "age": expected, } byteSlice, _ := json.Marshal(payload) je := jsonExtractor{} val, _ := je.extractFromByteSlice(byteSlice, "age") if !reflect.DeepEqual(val, expected) { t.Errorf("TestJsonExtract_JsonBoolArray failed, expected %#v, found %#v", expected, val) } val, _ = je.extractFromString(string(byteSlice), "age") if !reflect.DeepEqual(val, expected) { t.Errorf("TestJsonExtract_JsonBoolArray failed, expected %#v, found %#v", expected, val) } } func TestJsonExtract_JsonPathNotFound(t *testing.T) { payload := map[string]interface{}{ "age": "24", } byteSlice, _ := json.Marshal(payload) je := jsonExtractor{} val, err := je.extractFromByteSlice(byteSlice, "age2") expected := "no match for the json path: age2" if !strings.EqualFold(err.Error(), expected) { t.Errorf("TestJsonExtract_JsonPathNotFound failed, expected %#v, found %#v", expected, err) } if !reflect.DeepEqual(val, "") { t.Errorf("TestJsonExtract_JsonPathNotFound failed, expected %#v, found %#v", expected, val) } val, err = je.extractFromString(string(byteSlice), "age2") if !strings.EqualFold(err.Error(), expected) { t.Errorf("TestJsonExtract_JsonPathNotFound failed, expected %#v, found %#v", expected, err) } if !reflect.DeepEqual(val, "") { t.Errorf("TestJsonExtract_JsonPathNotFound failed, expected %#v, found %#v", expected, val) } } ================================================ FILE: ddosify_engine/core/scenario/scripting/extraction/regex.go ================================================ package extraction import ( "fmt" "regexp" ) type regexExtractor struct { r *regexp.Regexp } func (ri *regexExtractor) Init(regex string) { ri.r = regexp.MustCompile(regex) } func (ri *regexExtractor) extractFromString(text string, matchNo int) (string, error) { matches := ri.r.FindAllString(text, -1) if matches == nil { return "", fmt.Errorf("no match for the Regex: %s Match no: %d", ri.r.String(), matchNo) } if len(matches) > matchNo { return matches[matchNo], nil } return matches[0], nil } func (ri *regexExtractor) extractFromByteSlice(text []byte, matchNo int) ([]byte, error) { matches := ri.r.FindAll(text, -1) if matches == nil { return nil, fmt.Errorf("no match for the Regex: %s Match no: %d", ri.r.String(), matchNo) } if len(matches) > matchNo { return matches[matchNo], nil } return matches[0], nil } ================================================ FILE: ddosify_engine/core/scenario/scripting/extraction/regex_test.go ================================================ package extraction import ( "strings" "testing" ) func TestRegexExtractFromString(t *testing.T) { regex := "[a-z]+_[0-9]+" re := regexExtractor{} re.Init(regex) source := "messi_10alvarez_9" res, err2 := re.extractFromString(source, 1) if !strings.EqualFold(res, "alvarez_9") || err2 != nil { t.Errorf("RegexMatch should return second match") } res, err := re.extractFromString(source, 0) if !strings.EqualFold(res, "messi_10") || err != nil { t.Errorf("RegexMatch should return first match") } } func TestRegexExtractFromStringNoMatch(t *testing.T) { regex := "[a-z]+_[0-9]+" re := regexExtractor{} re.Init(regex) source := "messialvarez" _, err := re.extractFromString(source, 0) if err == nil { t.Errorf("Should be error %v", err) } } ================================================ FILE: ddosify_engine/core/scenario/scripting/extraction/xml.go ================================================ package extraction import ( "bytes" "fmt" "github.com/antchfx/xmlquery" ) type xmlExtractor struct { } func (xe xmlExtractor) extractFromByteSlice(source []byte, xPath string) (interface{}, error) { reader := bytes.NewBuffer(source) rootNode, err := xmlquery.Parse(reader) if err != nil { return nil, err } // returns the first matched element foundNode, err := xmlquery.Query(rootNode, xPath) if foundNode == nil || err != nil { return nil, fmt.Errorf("no match for the xPath: %s", xPath) } return foundNode.InnerText(), nil } func (xe xmlExtractor) extractFromString(source string, xPath string) (interface{}, error) { reader := bytes.NewBufferString(source) rootNode, err := xmlquery.Parse(reader) if err != nil { return nil, err } // returns the first matched element foundNode, err := xmlquery.Query(rootNode, xPath) if foundNode == nil || err != nil { return nil, fmt.Errorf("no match for this xpath") } return foundNode.InnerText(), nil } ================================================ FILE: ddosify_engine/core/scenario/scripting/extraction/xml_test.go ================================================ package extraction import ( "fmt" "strings" "testing" ) func TestXmlExtraction(t *testing.T) { expected := "XML Title" xmlSource := fmt.Sprintf(` %s `, expected) xe := xmlExtractor{} xpath := "//item/title" val, err := xe.extractFromByteSlice([]byte(xmlSource), xpath) if err != nil { t.Errorf("TestXmlExtraction %v", err) } if !strings.EqualFold(val.(string), expected) { t.Errorf("TestXmlExtraction expected: %s, got: %s", expected, val) } } func TestXmlExtractionString(t *testing.T) { expected := "XML Title" xmlSource := fmt.Sprintf(` %s `, expected) xe := xmlExtractor{} xpath := "//item/title" val, err := xe.extractFromString(xmlSource, xpath) if err != nil { t.Errorf("TestXmlExtraction %v", err) } if !strings.EqualFold(val.(string), expected) { t.Errorf("TestXmlExtraction expected: %s, got: %s", expected, val) } } func TestXmlExtraction_PathNotFound(t *testing.T) { expected := "XML Title" xmlSource := fmt.Sprintf(` %s `, expected) xe := xmlExtractor{} xpath := "//item3/title" _, err := xe.extractFromByteSlice([]byte(xmlSource), xpath) if err == nil { t.Errorf("TestXmlExtraction_PathNotFound, should be err, got :%v", err) } } func TestInvalidXml(t *testing.T) { xmlSource := `invalid xml source` xe := xmlExtractor{} xpath := "//item3/title" _, err := xe.extractFromByteSlice([]byte(xmlSource), xpath) if err == nil { t.Errorf("TestInvalidXml, should be err, got :%v", err) } } ================================================ FILE: ddosify_engine/core/scenario/scripting/injection/dynamic_test.go ================================================ package injection import ( "testing" ) func TestDynamicVariableRace(t *testing.T) { num := 10 ei := EnvironmentInjector{} for key := range dynamicFakeDataMap { for i := 0; i < num; i++ { go ei.getFakeData(key) } } } ================================================ FILE: ddosify_engine/core/scenario/scripting/injection/environment.go ================================================ package injection import ( "encoding/json" "fmt" "io" "math/rand" "os" "reflect" "regexp" "sort" "strings" "sync" "time" "unsafe" "go.ddosify.com/ddosify/core/types/regex" ) type BodyPiece struct { start int end int // end is not inclusive injectable bool value string // []byte // exist only if injectable is true } type DdosifyBodyReader struct { Body string // []byte Pieces []BodyPiece // keeps track of the current read position pi int // piece index vi int // index in the value of the current piece } // no-op close func (dbr *DdosifyBodyReader) Close() error { return nil } func (dbr *DdosifyBodyReader) Read(dst []byte) (n int, err error) { leftSpaceOnDst := len(dst) // assume dst is empty, so we can write to it from the beginning var readUntilPieceIndex int var readUntilPieceValueIndex int readUntilPieceIndex = dbr.pi readUntilPieceValueIndex = dbr.vi // find the piece index and value index to read until for leftSpaceOnDst > 0 && readUntilPieceIndex < len(dbr.Pieces) { var unReadOnCurrentPiece int piece := dbr.Pieces[readUntilPieceIndex] if piece.injectable { // has injected value unReadOnCurrentPiece = len(piece.value[readUntilPieceValueIndex:]) } else { unReadOnCurrentPiece = piece.end - piece.start - readUntilPieceValueIndex } if unReadOnCurrentPiece > leftSpaceOnDst { // will be a partial read // set readUntilPieceIndex and readUntilPieceValueIndex readUntilPieceValueIndex += leftSpaceOnDst leftSpaceOnDst = 0 } else { // will be a full read of the current piece // set readUntilPieceIndex and readUntilPieceValueIndex leftSpaceOnDst -= unReadOnCurrentPiece readUntilPieceValueIndex += unReadOnCurrentPiece if leftSpaceOnDst > 0 { // there is still space on dst readUntilPieceIndex++ readUntilPieceValueIndex = 0 } } } // continue reading from pieceIndex and valIndex // in first iteration, read from dbr.valIndex till the end of the piece // later on read from the beginning of the piece till the end of the piece for i := dbr.pi; i <= readUntilPieceIndex; i++ { if i == len(dbr.Pieces) { dbr.pi = i return n, io.EOF } piece := dbr.Pieces[i] if piece.injectable { // if dst has enough space to hold the whole piece // copy the whole piece from where we left off if len(dst[n:]) >= len(piece.value)-dbr.vi { copy(dst[n:n+len(piece.value)-dbr.vi], piece.value[dbr.vi:]) n += len(piece.value) - dbr.vi } else { // if dst does not have enough space to hold the whole piece // copy as much as we can and return leftSpaceOnDst := len(dst[n:]) copy(dst[n:], piece.value[dbr.vi:dbr.vi+leftSpaceOnDst]) n += leftSpaceOnDst dbr.pi = i dbr.vi = dbr.vi + leftSpaceOnDst return n, nil } } else { // if dst has enough space to hold the whole piece // copy the whole piece from where we left off if len(dst[n:]) >= piece.end-piece.start-dbr.vi { copy(dst[n:n+piece.end-piece.start-dbr.vi], dbr.Body[piece.start+dbr.vi:piece.end]) n += piece.end - piece.start - dbr.vi } else { // if dst does not have enough space to hold the whole piece // copy as much as we can and return leftSpaceOnDst := len(dst[n:]) copy(dst[n:], dbr.Body[piece.start+dbr.vi:piece.start+dbr.vi+leftSpaceOnDst]) n += leftSpaceOnDst dbr.pi = i dbr.vi = dbr.vi + leftSpaceOnDst return n, nil } } // jump to the next piece dbr.vi = 0 } // check if we have reached the end of the body if readUntilPieceIndex == len(dbr.Pieces)-1 { piece := dbr.Pieces[readUntilPieceIndex] if piece.injectable && readUntilPieceValueIndex == len(piece.value) { dbr.pi = readUntilPieceIndex + 1 // consumed the whole body return n, io.EOF } else if !piece.injectable && readUntilPieceValueIndex == piece.end-piece.start { dbr.pi = readUntilPieceIndex + 1 // consumed the whole body return n, io.EOF } } // readUntilPieceIndex is not the last piece, we fully filled dst // set where we left off dbr.pi = readUntilPieceIndex dbr.vi = readUntilPieceValueIndex return n, nil } type EnvironmentInjector struct { r *regexp.Regexp jr *regexp.Regexp dr *regexp.Regexp jdr *regexp.Regexp mu sync.Mutex } func (ei *EnvironmentInjector) Init() { ei.r = regexp.MustCompile(regex.EnvironmentVariableRegex) ei.jr = regexp.MustCompile(regex.JsonEnvironmentVarRegex) ei.dr = regexp.MustCompile(regex.DynamicVariableRegex) ei.jdr = regexp.MustCompile(regex.JsonDynamicVariableRegex) rand.Seed(time.Now().UnixNano()) } func truncateTag(tag string, rx string) string { if strings.EqualFold(rx, regex.EnvironmentVariableRegex) { return tag[2 : len(tag)-2] // {{...}} } else if strings.EqualFold(rx, regex.JsonEnvironmentVarRegex) { return tag[3 : len(tag)-3] // "{{...}}" } else if strings.EqualFold(rx, regex.DynamicVariableRegex) { return tag[3 : len(tag)-2] // {{_...}} } else if strings.EqualFold(rx, regex.JsonDynamicVariableRegex) { return tag[4 : len(tag)-3] //"{{_...}}" } return "" } func (ei *EnvironmentInjector) InjectEnv(text string, envs map[string]interface{}) (string, error) { errors := []error{} injectStrFunc := getInjectStrFunc(regex.EnvironmentVariableRegex, ei, envs, &errors) injectToJsonByteFunc := getInjectJsonFunc(regex.JsonEnvironmentVarRegex, ei, envs, &errors) // json injection bText := StringToBytes(text) if json.Valid(bText) { replacedBytes := ei.jr.ReplaceAllFunc(bText, injectToJsonByteFunc) if len(errors) == 0 { text = string(replacedBytes) } else { return "", unifyErrors(errors) } } // string injection replaced := ei.r.ReplaceAllStringFunc(text, injectStrFunc) if len(errors) == 0 { return replaced, nil } return replaced, unifyErrors(errors) } func (ei *EnvironmentInjector) getEnv(envs map[string]interface{}, key string) (interface{}, error) { var err error var val interface{} pickRand := strings.HasPrefix(key, "rand(") && strings.HasSuffix(key, ")") if pickRand { key = key[5 : len(key)-1] } var exists bool val, exists = envs[key] isOsEnv := strings.HasPrefix(key, "$") if isOsEnv { varName := key[1:] val, exists = os.LookupEnv(varName) } if !exists { err = fmt.Errorf("env not found") } if pickRand { switch v := val.(type) { case []interface{}: val = v[rand.Intn(len(v))] case []string: val = v[rand.Intn(len(v))] case []bool: val = v[rand.Intn(len(v))] case []int: val = v[rand.Intn(len(v))] case []float64: val = v[rand.Intn(len(v))] default: err = fmt.Errorf("can not perform rand() operation on non-array value") } } return val, err } func unifyErrors(errors []error) error { sb := strings.Builder{} for _, err := range errors { sb.WriteString(err.Error()) } return fmt.Errorf("%s", sb.String()) } func StringToBytes(s string) (b []byte) { stringHeader := (*reflect.StringHeader)(unsafe.Pointer(&s)) sliceHeader := (*reflect.SliceHeader)(unsafe.Pointer(&b)) sliceHeader.Data = stringHeader.Data sliceHeader.Len = len(s) sliceHeader.Cap = len(s) return b } func getInjectStrFunc(rx string, ei *EnvironmentInjector, envs map[string]interface{}, errors *[]error, ) func(string) string { return func(s string) string { var truncated string var env interface{} var err error truncated = truncateTag(string(s), rx) if rx == regex.EnvironmentVariableRegex { env, err = ei.getEnv(envs, truncated) } else if rx == regex.DynamicVariableRegex { env, err = ei.getFakeData(truncated) } else { // this should never happen panic("invalid regex") } if err == nil { switch env.(type) { case string: return env.(string) case []byte: return string(env.([]byte)) case int64: return fmt.Sprintf("%d", env) case int: return fmt.Sprintf("%d", env) case float64: return fmt.Sprintf("%g", env) // %g it is the smallest number of digits necessary to identify the value uniquely case bool: return fmt.Sprintf("%t", env) default: return fmt.Sprint(env) } } *errors = append(*errors, fmt.Errorf("%s could not be found in vars global and extracted from previous steps", truncated)) return s } } func getInjectJsonFunc(rx string, ei *EnvironmentInjector, envs map[string]interface{}, errors *[]error, ) func(s []byte) []byte { return func(s []byte) []byte { var truncated string var env interface{} var err error truncated = truncateTag(string(s), rx) if rx == regex.JsonDynamicVariableRegex { env, err = ei.getFakeData(truncated) } else if rx == regex.JsonEnvironmentVarRegex { env, err = ei.getEnv(envs, truncated) } else { // this should never happen panic("invalid regex") } if err == nil { mEnv, err := json.Marshal(env) if err == nil { return mEnv } } *errors = append(*errors, fmt.Errorf("%s could not be found in vars global and extracted from previous steps", truncated)) return s } } type EnvMatch struct { regex string // matched regex found []int // indexes of match } type EnvMatchSlice []EnvMatch func (a EnvMatchSlice) Len() int { return len(a) } func (a EnvMatchSlice) Swap(i, j int) { a[i], a[j] = a[j], a[i] } func (a EnvMatchSlice) Less(i, j int) bool { return a[i].found[0] < a[j].found[0] } func (ei *EnvironmentInjector) GenerateBodyPieces(body string, envs map[string]interface{}) []BodyPiece { // generate body pieces pieces := make([]BodyPiece, 0) matches := EnvMatchSlice{} bText := StringToBytes(body) if json.Valid(bText) { jsonEnvMatches := ei.jr.FindAllStringIndex(body, -1) for _, match := range jsonEnvMatches { matches = append(matches, EnvMatch{ regex: regex.JsonEnvironmentVarRegex, found: match, }) } envsInJsonStringMatches := ei.r.FindAllStringIndex(body, -1) for _, match := range envsInJsonStringMatches { // exclude ones that are already matched as json envs alreadyMatched := false for _, jsonMatch := range jsonEnvMatches { if match[0] >= jsonMatch[0] && match[1] <= jsonMatch[1] { alreadyMatched = true break } } if alreadyMatched { continue } matches = append(matches, EnvMatch{ regex: regex.EnvironmentVariableRegex, found: match, }) } jsonDynamicMatches := ei.jdr.FindAllStringIndex(body, -1) for _, match := range jsonDynamicMatches { matches = append(matches, EnvMatch{ regex: regex.JsonDynamicVariableRegex, found: match, }) } dynamicEnvsInJsonStringMatches := ei.dr.FindAllStringIndex(body, -1) for _, match := range dynamicEnvsInJsonStringMatches { // exclude ones that are already matched as json dynamic envs alreadyMatched := false for _, jsonMatch := range jsonDynamicMatches { if match[0] >= jsonMatch[0] && match[1] <= jsonMatch[1] { alreadyMatched = true break } } if alreadyMatched { continue } matches = append(matches, EnvMatch{ regex: regex.DynamicVariableRegex, found: match, }) } } else { // not json envMatches := ei.r.FindAllStringIndex(body, -1) for _, match := range envMatches { matches = append(matches, EnvMatch{ regex: regex.EnvironmentVariableRegex, found: match, }) } dynamicMathces := ei.dr.FindAllStringIndex(body, -1) for _, match := range dynamicMathces { matches = append(matches, EnvMatch{ regex: regex.DynamicVariableRegex, found: match, }) } } sort.Sort(matches) // by start index errors := make([]error, 0) off := 0 for _, match := range matches { r := match.regex start := match.found[0] end := match.found[1] if start > off { pieces = append(pieces, BodyPiece{ start: off, end: start, injectable: false, // value: body[off:match[0]], // no need to put values in here }) } f := getInjectStrFunc(regex.EnvironmentVariableRegex, ei, envs, &errors) fd := getInjectStrFunc(regex.DynamicVariableRegex, ei, nil, &errors) jf := getInjectJsonFunc(regex.JsonEnvironmentVarRegex, ei, envs, &errors) jfd := getInjectJsonFunc(regex.JsonDynamicVariableRegex, ei, nil, &errors) getValue := func(s string, r string) string { if r == regex.JsonEnvironmentVarRegex { return string(jf(StringToBytes(s))) } else if r == regex.JsonDynamicVariableRegex { return string(jfd(StringToBytes(s))) } else if r == regex.EnvironmentVariableRegex { return f(s) } else if r == regex.DynamicVariableRegex { return fd(s) } return s // this should never happen } val := getValue(body[start:end], r) pieces = append(pieces, BodyPiece{ start: start, end: end, injectable: true, value: val, // only put values in injected pieces }) off = end } if off < len(body) { pieces = append(pieces, BodyPiece{ start: off, end: len(body), injectable: false, // value: body[off:], }) } return pieces } func GetContentLength(pieces []BodyPiece) int { var contentLength int for _, piece := range pieces { if piece.injectable { contentLength += len(piece.value) } else { contentLength += piece.end - piece.start } } return contentLength } ================================================ FILE: ddosify_engine/core/scenario/scripting/injection/environment_dynamic.go ================================================ package injection import ( "encoding/json" "fmt" "reflect" "go.ddosify.com/ddosify/core/types/regex" ) func (ei *EnvironmentInjector) InjectDynamic(text string) (string, error) { errors := []error{} injectStrFunc := getInjectStrFunc(regex.DynamicVariableRegex, ei, nil, &errors) injectToJsonByteFunc := getInjectJsonFunc(regex.JsonDynamicVariableRegex, ei, nil, &errors) // json injection bText := StringToBytes(text) if json.Valid(bText) { if ei.jr.Match(bText) { replacedBytes := ei.jdr.ReplaceAllFunc(bText, injectToJsonByteFunc) return string(replacedBytes), nil } } // string injection replaced := ei.dr.ReplaceAllStringFunc(text, injectStrFunc) if len(errors) == 0 { return replaced, nil } return replaced, unifyErrors(errors) } func (ei *EnvironmentInjector) getFakeData(key string) (interface{}, error) { var fakeFunc interface{} var keyExists bool if fakeFunc, keyExists = dynamicFakeDataMap[key]; !keyExists { return nil, fmt.Errorf("%s is not a valid dynamic variable", key) } preventRaceOnRandomFunc := func(fakeFunc interface{}) interface{} { ei.mu.Lock() defer ei.mu.Unlock() return reflect.ValueOf(fakeFunc).Call(nil)[0].Interface() } return preventRaceOnRandomFunc(fakeFunc), nil } ================================================ FILE: ddosify_engine/core/scenario/scripting/injection/environment_test.go ================================================ package injection import ( "bytes" "encoding/json" "fmt" "io" "os" "reflect" "testing" "github.com/google/uuid" ) func TestInjectionRegexReplacer(t *testing.T) { replacer := EnvironmentInjector{} replacer.Init() // injection to text target targetURL := "{{target}}/{{path}}/{{id}}/{{boolField}}/{{floatField}}/{{uuidField}}" uuid := uuid.New() stringEnvs := map[string]interface{}{ "target": "https://app.ddosify.com", "path": "load/test-results", "id": 234, "boolField": true, "floatField": 22.3, "uuidField": uuid, } expectedURL := "https://app.ddosify.com/load/test-results/234/true/22.3/" + uuid.String() // injection to flat json target targetJson := `{ "{{a}}": 5, "name": "{{xyz}}", "numbers": "{{listOfNumbers}}", "chars": "{{object}}", "boolField": "{{boolEnv}}", "intField": "{{intEnv}}", "floatField": "{{floatEnv}}" }` jsonEnvs := map[string]interface{}{ "a": "age", "xyz": "kenan", "listOfNumbers": []float64{23, 44, 11}, "object": map[string]interface{}{"abc": []string{"a", "b", "c"}}, "boolEnv": false, "intEnv": 52, "floatEnv": 52.24, } expectedJsonPayload := `{ "age": 5, "name": "kenan", "numbers": [23,44,11], "chars": {"abc":["a","b","c"]}, "boolField": false, "intField": 52, "floatField": 52.24 }` // injection to recusive json target jsonRecursivePaylaod := `{ "chars": "{{object}}", "nc": {"max": "{{numVerstappen}}"} }` recursiveJsonEnvs := map[string]interface{}{ "object": map[string]interface{}{"abc": map[string]interface{}{"a": 1, "b": 1, "c": 1}}, "numVerstappen": 33, } expectedRecursiveJsonPayload := `{ "chars": {"abc":{"a":1,"b":1,"c":1}}, "nc": {"max": 33} }` // Sub Tests tests := []struct { name string target string expected interface{} envs map[string]interface{} }{ {"String", targetURL, expectedURL, stringEnvs}, {"JSONFlat", targetJson, expectedJsonPayload, jsonEnvs}, {"JSONRecursive", jsonRecursivePaylaod, expectedRecursiveJsonPayload, recursiveJsonEnvs}, } for _, test := range tests { tf := func(t *testing.T) { buff, err := replacer.InjectEnv(test.target, test.envs) if err != nil { t.Errorf("injection failed %v", err) } if !reflect.DeepEqual(buff, test.expected) { t.Errorf("injection unsuccessful, expected : %s, got :%s", test.expected, buff) } } t.Run(test.name, tf) } } func ExampleEnvironmentInjector() { replacer := EnvironmentInjector{} replacer.Init() res, err := replacer.InjectDynamic("{{_randomInt}}") if err == nil { fmt.Println(res) } } func TestRandomInjectionStringSlice(t *testing.T) { replacer := EnvironmentInjector{} replacer.Init() vals := []string{ "Kenan", "Kursat", "Fatih", } envs := map[string]interface{}{ "vals": vals, } val, err := replacer.getEnv(envs, "rand(vals)") if err != nil { t.Errorf("%v", err) } found := false for _, n := range vals { if reflect.DeepEqual(val, n) { found = true break } } if !found { t.Errorf("rand method did not return one of the expecteds") } } func TestRandomInjectionBoolSlice(t *testing.T) { replacer := EnvironmentInjector{} replacer.Init() vals := []bool{ true, false, true, } envs := map[string]interface{}{ "vals": vals, } val, err := replacer.getEnv(envs, "rand(vals)") if err != nil { t.Errorf("%v", err) } found := false for _, n := range vals { if reflect.DeepEqual(val, n) { found = true break } } if !found { t.Errorf("rand method did not return one of the expecteds") } } func TestRandomInjectionIntSlice(t *testing.T) { replacer := EnvironmentInjector{} replacer.Init() vals := []int{ 3, 55, 42, } envs := map[string]interface{}{ "vals": vals, } val, err := replacer.getEnv(envs, "rand(vals)") if err != nil { t.Errorf("%v", err) } found := false for _, n := range vals { if reflect.DeepEqual(val, n) { found = true break } } if !found { t.Errorf("rand method did not return one of the expecteds") } } func TestRandomInjectionFloat64Slice(t *testing.T) { replacer := EnvironmentInjector{} replacer.Init() vals := []float64{ 3.3, 55.23, 42.1, } envs := map[string]interface{}{ "vals": vals, } val, err := replacer.getEnv(envs, "rand(vals)") if err != nil { t.Errorf("%v", err) } found := false for _, n := range vals { if reflect.DeepEqual(val, n) { found = true break } } if !found { t.Errorf("rand method did not return one of the expecteds") } } func TestRandomInjectionInterfaceSlice(t *testing.T) { replacer := EnvironmentInjector{} replacer.Init() vals := []interface{}{ map[string]int{"s": 33}, []string{"v", "c"}, } envs := map[string]interface{}{ "vals": vals, } val, err := replacer.getEnv(envs, "rand(vals)") if err != nil { t.Errorf("%v", err) } found := false for _, n := range vals { if reflect.DeepEqual(val, n) { found = true break } } if !found { t.Errorf("rand method did not return one of the expecteds") } } func TestConcatVariablesAndInjectAsTyped(t *testing.T) { replacer := EnvironmentInjector{} replacer.Init() // injection to json payload payload := `{"a":["--{{number_int}}--{{number_string}}--","23","{{number_int}}"]}` envs := map[string]interface{}{ "number_int": 1, "number_string": "2", } expectedPayload := `{"a":["--1--2--","23",1]}` expected := &bytes.Buffer{} if err := json.Compact(expected, []byte(expectedPayload)); err != nil { panic(err) } pieces := replacer.GenerateBodyPieces(payload, envs) r := DdosifyBodyReader{ Body: payload, Pieces: pieces, } res := make([]byte, 100) n, err := r.Read(res) if err != io.EOF { t.Errorf("injection unsuccessful, expected : %s, got :%s", expected.String(), res) } if !reflect.DeepEqual(string(res[0:n]), expected.String()) { t.Errorf("injection unsuccessful, expected : %s, got :%s", expected.String(), res) } } func TestConcatVariablesAndInjectAsTyped2(t *testing.T) { replacer := EnvironmentInjector{} replacer.Init() // injection to json payload payload := `{"a":["--{{number_int}}{{number_string}}--","23","{{number_int}}"]}` envs := map[string]interface{}{ "number_int": 1, "number_string": "2", } expectedPayload := `{"a":["--12--","23",1]}` expected := &bytes.Buffer{} if err := json.Compact(expected, []byte(expectedPayload)); err != nil { panic(err) } pieces := replacer.GenerateBodyPieces(payload, envs) r := DdosifyBodyReader{ Body: payload, Pieces: pieces, } res := make([]byte, 100) n, err := r.Read(res) if err != io.EOF { t.Errorf("injection unsuccessful, expected : %s, got :%s", expected.String(), res) } if !reflect.DeepEqual(string(res[0:n]), expected.String()) { t.Errorf("injection unsuccessful, expected : %s, got :%s", expected.String(), res) } } func TestConcatVariablesAndInjectAsTypedDynamic(t *testing.T) { replacer := EnvironmentInjector{} replacer.Init() // injection to json payload payload := `{"a":["--{{_randomInt}}--{{number_string}}--","23","{{number_int}}"]}` envs := map[string]interface{}{ "number_int": 1, "number_string": "2", } dynamicInjectFailPayload := `{"a":["--{{_randomInt}}--2--","23",1]}` notExpected := &bytes.Buffer{} if err := json.Compact(notExpected, []byte(dynamicInjectFailPayload)); err != nil { panic(err) } pieces := replacer.GenerateBodyPieces(payload, envs) r := DdosifyBodyReader{ Body: payload, Pieces: pieces, } res := make([]byte, 100) n, err := r.Read(res) if err != io.EOF { t.Error(err) } if reflect.DeepEqual(string(res[0:n]), notExpected.String()) { t.Errorf("injection unsuccessful, not expected : %s, got :%s", notExpected.String(), res) } } func TestInvalidDynamicVarInjection(t *testing.T) { text := "http://test.com/{{_invalidVar}}" replacer := EnvironmentInjector{} replacer.Init() _, err := replacer.InjectDynamic(text) if err == nil { t.Errorf("expected error not found") } } func TestOSEnvInjection(t *testing.T) { replacer := EnvironmentInjector{} replacer.Init() actualEnvVal := os.Getenv("PATH") envs := map[string]interface{}{ "key1": "val1", } val, err := replacer.getEnv(envs, "$PATH") if err != nil { t.Errorf("%v", err) } found := false if reflect.DeepEqual(actualEnvVal, val) { found = true } if !found { t.Errorf("expected os env val not found") } } func TestDdosifyBodyReader(t *testing.T) { body := "test{{env1}}xyz{{env2}}" // only for env vars for now ei := EnvironmentInjector{} ei.Init() envs := make(map[string]interface{}) envs["env1"] = "123" envs["env2"] = "456" pieces := ei.GenerateBodyPieces(body, envs) customReader := DdosifyBodyReader{ Body: body, Pieces: pieces, } byteArray := make([]byte, GetContentLength(pieces)) n, err := customReader.Read(byteArray) // expect EOF if err != io.EOF { t.Errorf("expected EOF, got %v", err) } if n != GetContentLength(pieces) { t.Errorf("expected to read %d bytes, read %d", GetContentLength(pieces), n) } if string(byteArray) != "test123xyz456" { t.Errorf("expected test123xyz456, got %s", string(byteArray)) } } func TestDdosifyBodyReaderSplitted(t *testing.T) { body := "test{{env1}}xyz{{env2}}" // only for env vars for now ei := EnvironmentInjector{} ei.Init() envs := make(map[string]interface{}) envs["env1"] = "123" envs["env2"] = "456" pieces := ei.GenerateBodyPieces(body, envs) customReader := DdosifyBodyReader{ Body: body, Pieces: pieces, } firstPart := make([]byte, GetContentLength(pieces)-5) n, err := customReader.Read(firstPart) // do not expect EOF here if err != nil { t.Errorf("expected no error, got %v", err) } if n != GetContentLength(pieces)-5 { t.Errorf("expected to read %d bytes, read %d", GetContentLength(pieces)-5, n) } if string(firstPart) != "test123x" { t.Errorf("expected test123x, got %s", string(firstPart)) } secondPart := make([]byte, 5) n, err = customReader.Read(secondPart) // expect EOF here if err != io.EOF { t.Errorf("expected EOF, got %v", err) } if n != 5 { t.Errorf("expected to read %d bytes, read %d", 5, n) } if string(secondPart) != "yz456" { t.Errorf("expected yz456, got %s", string(secondPart)) } } func TestDdosifyBodyReaderSplittedPiece(t *testing.T) { body := "test{{env1}}xyz{{env2}}" // only for env vars for now ei := EnvironmentInjector{} ei.Init() envs := make(map[string]interface{}) envs["env1"] = "123" envs["env2"] = "456" pieces := ei.GenerateBodyPieces(body, envs) customReader := DdosifyBodyReader{ Body: body, Pieces: pieces, } firstPart := make([]byte, 2) n, err := customReader.Read(firstPart) // do not expect EOF here if err != nil { t.Errorf("expected no error, got %v", err) } if n != 2 { t.Errorf("expected to read %d bytes, read %d", GetContentLength(pieces)-5, n) } if string(firstPart) != "te" { t.Errorf("expected te, got %s", string(firstPart)) } secondPart := make([]byte, GetContentLength(pieces)-2) n, err = customReader.Read(secondPart) // expect EOF here if err != io.EOF { t.Errorf("expected EOF, got %v", err) } if n != GetContentLength(pieces)-2 { t.Errorf("expected to read %d bytes, read %d", GetContentLength(pieces)-2, n) } if string(secondPart) != "st123xyz456" { t.Errorf("expected st123xyz456, got %s", string(secondPart)) } } func TestDdosifyBodyReaderSplittedPiece2(t *testing.T) { body := "test{{env1}}xyz{{env2}}" // only for env vars for now ei := EnvironmentInjector{} ei.Init() envs := make(map[string]interface{}) envs["env1"] = "123" envs["env2"] = "456" pieces := ei.GenerateBodyPieces(body, envs) customReader := DdosifyBodyReader{ Body: body, Pieces: pieces, } firstPart := make([]byte, 2) n, err := customReader.Read(firstPart) // do not expect EOF here if err != nil { t.Errorf("expected no error, got %v", err) } if n != 2 { t.Errorf("expected to read %d bytes, read %d", GetContentLength(pieces)-5, n) } if string(firstPart) != "te" { t.Errorf("expected te, got %s", string(firstPart)) } secondPart := make([]byte, GetContentLength(pieces)) n, err = customReader.Read(secondPart) // expect EOF here if err != io.EOF { t.Errorf("expected EOF, got %v", err) } if n != GetContentLength(pieces)-2 { t.Errorf("expected to read %d bytes, read %d", GetContentLength(pieces)-2, n) } if string(secondPart[0:n]) != "st123xyz456" { t.Errorf("expected st123xyz456, got %s", string(secondPart)) } // try to read again, should be EOF emptyPart := make([]byte, GetContentLength(pieces)) n, err = customReader.Read(emptyPart) if n != 0 { t.Errorf("expected to read %d bytes, read %d", 0, n) } if err != io.EOF { t.Errorf("expected EOF, got %v", err) } } func TestDdosifyBodyReaderSplittedPiece3(t *testing.T) { body := "test{{env1}}xyz" // only for env vars for now ei := EnvironmentInjector{} ei.Init() envs := make(map[string]interface{}) envs["env1"] = "123" pieces := ei.GenerateBodyPieces(body, envs) customReader := DdosifyBodyReader{ Body: body, Pieces: pieces, } firstPart := make([]byte, 2) n, err := customReader.Read(firstPart) // do not expect EOF here if err != nil { t.Errorf("expected no error, got %v", err) } if n != 2 { t.Errorf("expected to read %d bytes, read %d", GetContentLength(pieces)-5, n) } if string(firstPart) != "te" { t.Errorf("expected te, got %s", string(firstPart)) } secondPart := make([]byte, 5) n, err = customReader.Read(secondPart) // fully read the second part, no EOF if err == io.EOF { t.Errorf("expected no EOF, got %v", err) } if n != 5 { t.Errorf("expected to read %d bytes, read %d", 5, n) } if string(secondPart[0:n]) != "st123" { t.Errorf("expected st123, got %s", string(secondPart)) } // try to read again, should be EOF lastPart := make([]byte, GetContentLength(pieces)) n, err = customReader.Read(lastPart) if n != 3 { t.Errorf("expected to read %d bytes, read %d", 0, n) } if err != io.EOF { t.Errorf("expected EOF, got %v", err) } } func TestDdosifyBodyReaderSplittedPiece4(t *testing.T) { body := "test{{env1}}{{env2}}" // only for env vars for now ei := EnvironmentInjector{} ei.Init() envs := make(map[string]interface{}) envs["env1"] = "123" envs["env2"] = "456" pieces := ei.GenerateBodyPieces(body, envs) customReader := DdosifyBodyReader{ Body: body, Pieces: pieces, } firstPart := make([]byte, 2) n, err := customReader.Read(firstPart) // do not expect EOF here if err != nil { t.Errorf("expected no error, got %v", err) } if n != 2 { t.Errorf("expected to read %d bytes, read %d", GetContentLength(pieces)-5, n) } if string(firstPart) != "te" { t.Errorf("expected te, got %s", string(firstPart)) } secondPart := make([]byte, 5) n, err = customReader.Read(secondPart) // fully read the second part, no EOF if err == io.EOF { t.Errorf("expected no EOF, got %v", err) } if n != 5 { t.Errorf("expected to read %d bytes, read %d", 5, n) } if string(secondPart[0:n]) != "st123" { t.Errorf("expected st123, got %s", string(secondPart)) } // try to read again, should be EOF lastPart := make([]byte, GetContentLength(pieces)) n, err = customReader.Read(lastPart) if n != 3 { t.Errorf("expected to read %d bytes, read %d", 0, n) } if err != io.EOF { t.Errorf("expected EOF, got %v", err) } } func TestDdosifyBodyReaderSplittedPiece5(t *testing.T) { body := "test{{env1}}xyz" // only for env vars for now ei := EnvironmentInjector{} ei.Init() envs := make(map[string]interface{}) envs["env1"] = "123" pieces := ei.GenerateBodyPieces(body, envs) customReader := DdosifyBodyReader{ Body: body, Pieces: pieces, } firstPart := make([]byte, 2) n, err := customReader.Read(firstPart) // do not expect EOF here if err != nil { t.Errorf("expected no error, got %v", err) } if n != 2 { t.Errorf("expected to read %d bytes, read %d", GetContentLength(pieces)-5, n) } if string(firstPart) != "te" { t.Errorf("expected te, got %s", string(firstPart)) } secondPart := make([]byte, 5) n, err = customReader.Read(secondPart) // fully read the second part, no EOF if err == io.EOF { t.Errorf("expected no EOF, got %v", err) } if n != 5 { t.Errorf("expected to read %d bytes, read %d", 5, n) } if string(secondPart[0:n]) != "st123" { t.Errorf("expected st123, got %s", string(secondPart)) } // try to read again, should be EOF lastPart := make([]byte, 3) n, err = customReader.Read(lastPart) if n != 3 { t.Errorf("expected to read %d bytes, read %d", 0, n) } if err != io.EOF { t.Errorf("expected EOF, got %v", err) } } func TestGenerateBodyPieces(t *testing.T) { body := "test{{env1}}xyz{{env2}}" // only for env vars for now ei := EnvironmentInjector{} ei.Init() envs := make(map[string]interface{}) envs["env1"] = "123" envs["env2"] = "456" pieces := ei.GenerateBodyPieces(body, envs) if len(pieces) != 4 { t.Errorf("expected 4 pieces, got %d", len(pieces)) } if pieces[0].start != 0 && pieces[0].end != 4 { t.Errorf("expected start 0 and end 4, got %d and %d", pieces[0].start, pieces[0].end) } if pieces[1].start != 4 && pieces[1].end != 12 { t.Errorf("expected start 4 and end 12, got %d and %d", pieces[1].start, pieces[1].end) } if pieces[2].start != 12 && pieces[2].end != 15 { t.Errorf("expected start 12 and end 15, got %d and %d", pieces[2].start, pieces[2].end) } if pieces[3].start != 15 && pieces[3].end != 23 { t.Errorf("expected start 15 and end 23, got %d and %d", pieces[3].start, pieces[3].end) } if !pieces[1].injectable { t.Errorf("expected piece 1 to be injectable") } if !pieces[3].injectable { t.Errorf("expected piece 3 to be injectable") } if pieces[0].injectable { t.Errorf("expected piece 0 to not be injectable") } if pieces[2].injectable { t.Errorf("expected piece 2 to not be injectable") } if pieces[1].value != "123" { t.Errorf("expected piece 1 value to be 123") } if pieces[3].value != "456" { t.Errorf("expected piece 3 value to be 456") } // test content length // 4 + {8} + 3 + {8} = 23 // 4 + {3} + 3 + {3} = 13 if GetContentLength(pieces) != 13 { t.Errorf("expected content length to be 13") } } func TestGenerateBodyPiecesWithDynamicVars(t *testing.T) { body := "test{{env1}}xyz{{_randomInt}}" ei := EnvironmentInjector{} ei.Init() envs := make(map[string]interface{}) envs["env1"] = "123" pieces := ei.GenerateBodyPieces(body, envs) if len(pieces) != 4 { t.Errorf("expected 4 pieces, got %d", len(pieces)) } if pieces[0].start != 0 && pieces[0].end != 4 { t.Errorf("expected start 0 and end 4, got %d and %d", pieces[0].start, pieces[0].end) } if pieces[1].start != 4 && pieces[1].end != 12 { t.Errorf("expected start 4 and end 12, got %d and %d", pieces[1].start, pieces[1].end) } if pieces[2].start != 12 && pieces[2].end != 15 { t.Errorf("expected start 12 and end 15, got %d and %d", pieces[2].start, pieces[2].end) } if pieces[3].start != 15 && pieces[3].end != 15+len(pieces[3].value) { t.Errorf("expected start 15 and end %d, got %d and %d", 15+len(pieces[3].value), pieces[3].start, pieces[3].end) } if !pieces[1].injectable { t.Errorf("expected piece 1 to be injectable") } if !pieces[3].injectable { t.Errorf("expected piece 3 to be injectable") } if pieces[0].injectable { t.Errorf("expected piece 0 to not be injectable") } if pieces[2].injectable { t.Errorf("expected piece 2 to not be injectable") } if pieces[1].value != "123" { t.Errorf("expected piece 1 value to be 123") } // it will be random, so we can't test it // if pieces[3].value != "456" { // t.Errorf("expected piece 3 value to be 456") // } } func TestGenerateBodyPiecesSorted(t *testing.T) { body := "test{{_randomInt}}xyz{{env1}}{{env2}}{{_randomCity}}" ei := EnvironmentInjector{} ei.Init() envs := make(map[string]interface{}) envs["env1"] = "123" envs["env2"] = "777" pieces := ei.GenerateBodyPieces(body, envs) if len(pieces) != 6 { t.Errorf("expected 6 pieces, got %d", len(pieces)) } for i := 0; i < len(pieces)-1; i++ { if pieces[i].start > pieces[i+1].start { t.Errorf("expected pieces to be sorted by start") } if pieces[i].end > pieces[i+1].end { t.Errorf("expected pieces to be sorted by end") } if pieces[i].end != pieces[i+1].start { t.Errorf("expected pieces to be contiguous") } } } ================================================ FILE: ddosify_engine/core/scenario/scripting/injection/init.go ================================================ package injection import "github.com/ddosify/go-faker/faker" var dynamicFakeDataMap map[string]interface{} var dataFaker faker.Faker func init() { dataFaker = faker.NewFaker() dynamicFakeDataMap = map[string]interface{}{ /* * Postman equivalents: https://learning.postman.com/docs/writing-scripts/script-references/variables-list */ // Common "guid": dataFaker.RandomGuid, "timestamp": dataFaker.CurrentTimestamp, "isoTimestamp": dataFaker.CurrentISOTimestamp, "randomUUID": dataFaker.RandomUUID, //Text, numbers, and colors "randomAlphaNumeric": dataFaker.RandomAlphanumeric, "randomBoolean": dataFaker.RandomBoolean, "randomInt": dataFaker.RandomInt, "randomColor": dataFaker.RandomSafeColorName, "randomHexColor": dataFaker.RandomSafeColorHex, "randomAbbreviation": dataFaker.RandomAbbreviation, // Internet and IP addresses "randomIP": dataFaker.RandomIP, "randomIPV6": dataFaker.RandomIpv6, "randomMACAddress": dataFaker.RandomMACAddress, "randomPassword": dataFaker.RandomPassword, "randomLocale": dataFaker.RandomLocale, "randomUserAgent": dataFaker.RandomUserAgent, "randomProtocol": dataFaker.RandomProtocol, "randomSemver": dataFaker.RandomSemver, // Names "randomFirstName": dataFaker.RandomPersonFirstName, "randomLastName": dataFaker.RandomPersonLastName, "randomFullName": dataFaker.RandomPersonFullName, "randomNamePrefix": dataFaker.RandomPersonNamePrefix, "randomNameSuffix": dataFaker.RandomPersonNameSuffix, // Profession "randomJobArea": dataFaker.RandomJobArea, "randomJobDescriptor": dataFaker.RandomJobDescriptor, "randomJobTitle": dataFaker.RandomJobTitle, "randomJobType": dataFaker.RandomJobType, // Phone, address, and location "randomPhoneNumber": dataFaker.RandomPhoneNumber, "randomPhoneNumberExt": dataFaker.RandomPhoneNumberExt, "randomCity": dataFaker.RandomAddressCity, "randomStreetName": dataFaker.RandomAddresStreetName, "randomStreetAddress": dataFaker.RandomAddressStreetAddress, "randomCountry": dataFaker.RandomAddressCountry, "randomCountryCode": dataFaker.RandomCountryCode, "randomLatitude": dataFaker.RandomAddressLatitude, "randomLongitude": dataFaker.RandomAddressLongitude, // Images "randomAvatarImage": dataFaker.RandomAvatarImage, "randomImageUrl": dataFaker.RandomImageURL, "randomAbstractImage": dataFaker.RandomAbstractImage, "randomAnimalsImage": dataFaker.RandomAnimalsImage, "randomBusinessImage": dataFaker.RandomBusinessImage, "randomCatsImage": dataFaker.RandomCatsImage, "randomCityImage": dataFaker.RandomCityImage, "randomFoodImage": dataFaker.RandomFoodImage, "randomNightlifeImage": dataFaker.RandomNightlifeImage, "randomFashionImage": dataFaker.RandomFashionImage, "randomPeopleImage": dataFaker.RandomPeopleImage, "randomNatureImage": dataFaker.RandomNatureImage, "randomSportsImage": dataFaker.RandomSportsImage, "randomTransportImage": dataFaker.RandomTransportImage, "randomImageDataUri": dataFaker.RandomDataImageUri, // Finance "randomBankAccount": dataFaker.RandomBankAccount, "randomBankAccountName": dataFaker.RandomBankAccountName, "randomCreditCardMask": dataFaker.RandomCreditCardMask, "randomBankAccountBic": dataFaker.RandomBankAccountBic, "randomBankAccountIban": dataFaker.RandomBankAccountIban, "randomTransactionType": dataFaker.RandomTransactionType, "randomCurrencyCode": dataFaker.RandomCurrencyCode, "randomCurrencyName": dataFaker.RandomCurrencyName, "randomCurrencySymbol": dataFaker.RandomCurrencySymbol, "randomBitcoin": dataFaker.RandomBitcoin, // Business "randomCompanyName": dataFaker.RandomCompanyName, "randomCompanySuffix": dataFaker.RandomCompanySuffix, "randomBs": dataFaker.RandomBs, "randomBsAdjective": dataFaker.RandomBsAdjective, "randomBsBuzz": dataFaker.RandomBsBuzzWord, "randomBsNoun": dataFaker.RandomBsNoun, // Catchphrases "randomCatchPhrase": dataFaker.RandomCatchPhrase, "randomCatchPhraseAdjective": dataFaker.RandomCatchPhraseAdjective, "randomCatchPhraseDescriptor": dataFaker.RandomCatchPhraseDescriptor, "randomCatchPhraseNoun": dataFaker.RandomCatchPhraseNoun, // Databases "randomDatabaseColumn": dataFaker.RandomDatabaseColumn, "randomDatabaseType": dataFaker.RandomDatabaseType, "randomDatabaseCollation": dataFaker.RandomDatabaseCollation, "randomDatabaseEngine": dataFaker.RandomDatabaseEngine, // Dates "randomDateFuture": dataFaker.RandomDateFuture, "randomDatePast": dataFaker.RandomDatePast, "randomDateRecent": dataFaker.RandomDateRecent, "randomWeekday": dataFaker.RandomWeekday, "randomMonth": dataFaker.RandomMonth, // Domains, emails, and usernames "randomDomainName": dataFaker.RandomDomainName, "randomDomainSuffix": dataFaker.RandomDomainSuffix, "randomDomainWord": dataFaker.RandomDomainWord, "randomEmail": dataFaker.RandomEmail, "randomExampleEmail": dataFaker.RandomExampleEmail, "randomUserName": dataFaker.RandomUsername, "randomUrl": dataFaker.RandomUrl, // Files and directories "randomFileName": dataFaker.RandomFileName, "randomFileType": dataFaker.RandomFileType, "randomFileExt": dataFaker.RandomFileExtension, "randomCommonFileName": dataFaker.RandomCommonFileName, "randomCommonFileType": dataFaker.RandomCommonFileType, "randomCommonFileExt": dataFaker.RandomCommonFileExtension, "randomFilePath": dataFaker.RandomFilePath, "randomDirectoryPath": dataFaker.RandomDirectoryPath, "randomMimeType": dataFaker.RandomMimeType, // Stores "randomPrice": dataFaker.RandomPrice, "randomProduct": dataFaker.RandomProduct, "randomProductAdjective": dataFaker.RandomProductAdjective, "randomProductMaterial": dataFaker.RandomProductMaterial, "randomProductName": dataFaker.RandomProductName, "randomDepartment": dataFaker.RandomDepartment, // Grammar "randomNoun": dataFaker.RandomNoun, "randomVerb": dataFaker.RandomVerb, "randomIngverb": dataFaker.RandomIngVerb, "randomAdjective": dataFaker.RandomAdjective, "randomWord": dataFaker.RandomWord, "randomWords": dataFaker.RandomWords, "randomPhrase": dataFaker.RandomPhrase, // Lorem ipsum "randomLoremWord": dataFaker.RandomLoremWord, "randomLoremWords": dataFaker.RandomLoremWords, "randomLoremSentence": dataFaker.RandomLoremSentence, "randomLoremSentences": dataFaker.RandomLoremSentences, "randomLoremParagraph": dataFaker.RandomLoremParagraph, "randomLoremParagraphs": dataFaker.RandomLoremParagraphs, "randomLoremText": dataFaker.RandomLoremText, "randomLoremSlug": dataFaker.RandomLoremSlug, "randomLoremLines": dataFaker.RandomLoremLines, /* * Specific to us. */ "randomFloat": dataFaker.RandomFloat, "randomString": dataFaker.RandomString, } } ================================================ FILE: ddosify_engine/core/scenario/service.go ================================================ /* * * Ddosify - Load testing tool for any web system. * Copyright (C) 2021 Ddosify (https://ddosify.com) * * This program is free software: you can redistribute it and/or modify * it under the terms of the GNU Affero General Public License as published * by the Free Software Foundation, either version 3 of the License, or * (at your option) any later version. * * This program is distributed in the hope that it will be useful, * but WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * GNU Affero General Public License for more details. * * You should have received a copy of the GNU Affero General Public License * along with this program. If not, see . * */ package scenario import ( "context" "fmt" "math/rand" "net/http" "net/url" "regexp" "strconv" "strings" "sync" "sync/atomic" "time" "go.ddosify.com/ddosify/core/scenario/requester" "go.ddosify.com/ddosify/core/scenario/scripting/injection" "go.ddosify.com/ddosify/core/types" "go.ddosify.com/ddosify/core/types/regex" "go.ddosify.com/ddosify/core/util" ) // ScenarioService encapsulates proxy/scenario/requester information and runs the scenario. type ScenarioService struct { // Client map structure [proxy_addr][]scenarioItemRequester // Each proxy represents a client. // Each scenarioItem has a requester clients map[*url.URL][]scenarioItemRequester cPool *util.Pool[*http.Client] scenario types.Scenario ctx context.Context clientMutex sync.Mutex debug bool engineMode string ei *injection.EnvironmentInjector iterIndex int64 } // NewScenarioService is the constructor of the ScenarioService. func NewScenarioService() *ScenarioService { return &ScenarioService{} } type ScenarioOpts struct { Debug bool IterationCount int MaxConcurrentIterCount int EngineMode string InitialCookies []*http.Cookie } // Init initializes the ScenarioService.clients with the given types.Scenario and proxies. // Passes the given ctx to the underlying requestor so we are able to control the life of each request. func (s *ScenarioService) Init(ctx context.Context, scenario types.Scenario, proxies []*url.URL, opts ScenarioOpts) (err error) { s.scenario = scenario s.ctx = ctx s.debug = opts.Debug s.clients = make(map[*url.URL][]scenarioItemRequester, len(proxies)) ei := &injection.EnvironmentInjector{} ei.Init() s.ei = ei for _, p := range proxies { err = s.createRequesters(p) if err != nil { return } } vi := &injection.EnvironmentInjector{} vi.Init() s.ei = vi s.engineMode = opts.EngineMode if s.engineInUserMode() { // create client pool var initialCount int var maxCount int if opts.Debug { // just one client initialCount = 1 maxCount = 1 } else if s.engineMode == types.EngineModeRepeatedUser { initialCount = opts.MaxConcurrentIterCount maxCount = opts.MaxConcurrentIterCount } else if s.engineMode == types.EngineModeDistinctUser { initialCount = opts.MaxConcurrentIterCount maxCount = opts.IterationCount } s.cPool, err = NewClientPool(initialCount, maxCount, s.engineMode, putInitialCookiesInJarFactory(s.engineMode, opts.InitialCookies), func(c *http.Client) { c.CloseIdleConnections() }) } // s.cPool will be nil otherwise return } func putInitialCookiesInJarFactory(engineMode string, initCookies []*http.Cookie) ClientFactoryMethod { return createClientFactoryMethod(engineMode, func(cj http.CookieJar) { for _, c := range initCookies { var scheme string = "http" if c.Secure { scheme = "https" } url := &url.URL{Host: c.Domain, Scheme: scheme} cj.SetCookies(url, []*http.Cookie{c}) } }) } // Do executes the scenario for the given proxy. // Returns "types.Response" filled by the requester of the given Proxy, injects the given startTime to the response // Returns error only if types.Response.Err.Type is types.ErrorProxy or types.ErrorIntented func (s *ScenarioService) Do(proxy *url.URL, startTime time.Time) ( response *types.ScenarioResult, err *types.RequestError) { response = &types.ScenarioResult{StepResults: []*types.ScenarioStepResult{}} response.StartTime = startTime response.ProxyAddr = proxy rand.Seed(time.Now().UnixNano()) requesters, e := s.getOrCreateRequesters(proxy) if e != nil { return nil, &types.RequestError{Type: types.ErrorUnkown, Reason: e.Error()} } // start envs separately for each iteration envs := make(map[string]interface{}, len(s.scenario.Envs)) for k, v := range s.scenario.Envs { envs[k] = v } // inject dynamic variables beforehand for each iteration injectDynamicVars(s.ei, envs) // pass a row from data for each iteration s.enrichEnvFromData(envs) atomic.AddInt64(&s.iterIndex, 1) var client *http.Client if s.engineInUserMode() { // get client from pool client = s.cPool.Get() defer s.cPool.Put(client) } for _, sr := range requesters { var res *types.ScenarioStepResult switch sr.requester.Type() { case "HTTP": httpRequester := sr.requester.(requester.HttpRequesterI) res = httpRequester.Send(client, envs) default: res = &types.ScenarioStepResult{Err: types.RequestError{Type: fmt.Sprintf("type not defined: %s", sr.requester.Type())}} } if res.Err.Type == types.ErrorProxy || res.Err.Type == types.ErrorIntented { err = &res.Err if res.Err.Type == types.ErrorIntented { // Stop the loop. ErrorProxy can be fixed in time. But ErrorIntented is a signal to stop all. return } } response.StepResults = append(response.StepResults, res) // Sleep before running the next step if sr.sleeper != nil && len(s.scenario.Steps) > 1 { sr.sleeper.sleep() } enrichEnvFromPrevStep(envs, res.ExtractedEnvs) } return } func enrichEnvFromPrevStep(m1 map[string]interface{}, m2 map[string]interface{}) { for k, v := range m2 { m1[k] = v } } func (s *ScenarioService) engineInUserMode() bool { if s.engineMode == types.EngineModeDistinctUser || s.engineMode == types.EngineModeRepeatedUser { return true } return false } func (s *ScenarioService) enrichEnvFromData(envs map[string]interface{}) { var row map[string]interface{} sb := strings.Builder{} for key, csvData := range s.scenario.Data { lenRows := len(csvData.Rows) if csvData.Random { row = csvData.Rows[rand.Intn(lenRows)] } else { row = csvData.Rows[s.iterIndex%int64(lenRows)] } for tag, v := range row { sb.WriteString("data.") sb.WriteString(key) sb.WriteString(".") sb.WriteString(tag) // data.info.name envs[sb.String()] = v sb.Reset() } } } func (s *ScenarioService) Done() { for _, v := range s.clients { for _, r := range v { r.requester.Done() } } if s.cPool != nil { s.cPool.Done() } } func (s *ScenarioService) getOrCreateRequesters(proxy *url.URL) (requesters []scenarioItemRequester, err error) { s.clientMutex.Lock() defer s.clientMutex.Unlock() requesters, ok := s.clients[proxy] if !ok { err = s.createRequesters(proxy) if err != nil { return } } return s.clients[proxy], err } func (s *ScenarioService) createRequesters(proxy *url.URL) (err error) { s.clients[proxy] = []scenarioItemRequester{} for _, si := range s.scenario.Steps { var r requester.Requester r, err = requester.NewRequester(si) if err != nil { return } s.clients[proxy] = append( s.clients[proxy], scenarioItemRequester{ scenarioItemID: si.ID, sleeper: newSleeper(si.Sleep), requester: r, }, ) switch r.Type() { case "HTTP": httpRequester := r.(requester.HttpRequesterI) err = httpRequester.Init(s.ctx, si, proxy, s.debug, s.ei) default: err = fmt.Errorf("type not defined: %s", r.Type()) } if err != nil { return } } return err } func injectDynamicVars(vi *injection.EnvironmentInjector, envs map[string]interface{}) { dynamicRgx := regexp.MustCompile(regex.DynamicVariableRegex) for k, v := range envs { vStr, isStr := v.(string) if !isStr { continue } if dynamicRgx.MatchString(vStr) { injected, err := vi.InjectDynamic(vStr) if err != nil { continue } envs[k] = injected } } } type scenarioItemRequester struct { scenarioItemID uint16 sleeper Sleeper requester requester.Requester } // Sleeper is the interface for implementing different sleep strategies. type Sleeper interface { sleep() } // RangeSleep is the implementation of the range sleep feature type RangeSleep struct { min int max int } func (rs *RangeSleep) sleep() { rand.Seed(time.Now().UnixNano()) dur := rand.Intn(rs.max-rs.min+1) + rs.min time.Sleep(time.Duration(dur) * time.Millisecond) } // DurationSleep is the implementation of the exact duration sleep feature type DurationSleep struct { duration int } func (ds *DurationSleep) sleep() { time.Sleep(time.Duration(ds.duration) * time.Millisecond) } // newSleeper is the factor method for the Sleeper implementations. func newSleeper(sleepStr string) Sleeper { if sleepStr == "" { return nil } var sl Sleeper // Sleep field already validated in types.scenario.validate(). No need to check parsing errors here. s := strings.Split(sleepStr, "-") if len(s) == 2 { min, _ := strconv.Atoi(s[0]) max, _ := strconv.Atoi(s[1]) if min > max { min, max = max, min } sl = &RangeSleep{ min: min, max: max, } } else { dur, _ := strconv.Atoi(s[0]) sl = &DurationSleep{ duration: dur, } } return sl } ================================================ FILE: ddosify_engine/core/scenario/service_test.go ================================================ /* * * Ddosify - Load testing tool for any web system. * Copyright (C) 2021 Ddosify (https://ddosify.com) * * This program is free software: you can redistribute it and/or modify * it under the terms of the GNU Affero General Public License as published * by the Free Software Foundation, either version 3 of the License, or * (at your option) any later version. * * This program is distributed in the hope that it will be useful, * but WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * GNU Affero General Public License for more details. * * You should have received a copy of the GNU Affero General Public License * along with this program. If not, see . * */ package scenario import ( "context" "fmt" "net/http" "net/url" "reflect" "testing" "time" "go.ddosify.com/ddosify/core/scenario/requester" "go.ddosify.com/ddosify/core/scenario/scripting/injection" "go.ddosify.com/ddosify/core/types" ) type MockHttpRequester struct { InitCalled bool SendCalled bool DoneCalled bool FailInit bool FailInitMsg string EnvsSet bool ReturnSend *types.ScenarioStepResult } func (m *MockHttpRequester) Init(ctx context.Context, s types.ScenarioStep, proxyAddr *url.URL, debug bool, ei *injection.EnvironmentInjector) (err error) { m.InitCalled = true if m.FailInit { return fmt.Errorf(m.FailInitMsg) } return } func (m *MockHttpRequester) Send(client *http.Client, envs map[string]interface{}) (res *types.ScenarioStepResult) { m.SendCalled = true return m.ReturnSend } func (m *MockHttpRequester) Done() { m.DoneCalled = true } func (m *MockHttpRequester) Type() string { return "HTTP" } type MockSleep struct { SleepCalled bool SleepCallCount int } func (msl *MockSleep) sleep() { msl.SleepCalled = true msl.SleepCallCount++ } func compareScenarioServiceClients( expectedClients map[*url.URL][]scenarioItemRequester, clients map[*url.URL][]scenarioItemRequester) error { if len(expectedClients) != len(clients) { return fmt.Errorf("[length] Expected %v, Found %v", expectedClients, clients) } for k, expectedVal := range expectedClients { val, ok := clients[k] if !ok { return fmt.Errorf("[key] Expected %#v, Found %#v", expectedClients, clients) } if len(expectedVal) != len(val) { return fmt.Errorf("[valLength] Expected %v, Found %v", expectedVal, val) } for i := 0; i < len(expectedVal); i++ { if expectedVal[i].scenarioItemID != val[i].scenarioItemID { return fmt.Errorf("[scenarioItemID] Expected %#v, Found %#v", expectedVal, val) } if expectedVal[i].scenarioItemID != val[i].scenarioItemID { return fmt.Errorf("[scenarioItemID] Expected %#v, Found %#v", expectedVal, val) } if reflect.TypeOf(expectedVal[i].requester) != reflect.TypeOf(val[i].requester) { return fmt.Errorf("[requester] Expected %#v, Found %#v", expectedVal, val) } if reflect.TypeOf(expectedVal[i].sleeper) != reflect.TypeOf(val[i].sleeper) { return fmt.Errorf("[sleep] Expected %#v, Found %#v", expectedVal, val) } if !reflect.DeepEqual(expectedVal[i].sleeper, val[i].sleeper) { return fmt.Errorf("[sleep] Expected %#v, Found %#v", expectedVal, val) } } } return nil } func TestInitService(t *testing.T) { t.Parallel() // Arrange scenario := types.Scenario{ Steps: []types.ScenarioStep{ { ID: 1, Method: types.DefaultMethod, URL: "test.com", Timeout: types.DefaultDuration, Sleep: "300-500", }, { ID: 2, Method: types.DefaultMethod, URL: "test2.com", Timeout: types.DefaultDuration, Sleep: "1000", }, { ID: 3, Method: types.DefaultMethod, URL: "test3.com", Timeout: types.DefaultDuration, }, }, } p1, _ := url.Parse("http://proxy_server.com:80") p2, _ := url.Parse("http://proxy_server2.com:8000") proxies := []*url.URL{p1, p2} ctx := context.TODO() expectedClients := map[*url.URL][]scenarioItemRequester{ p1: { { scenarioItemID: 1, requester: &requester.HttpRequester{}, sleeper: &RangeSleep{min: 300, max: 500}, }, { scenarioItemID: 2, requester: &requester.HttpRequester{}, sleeper: &DurationSleep{duration: 1000}, }, { scenarioItemID: 3, requester: &requester.HttpRequester{}, }, }, p2: { { scenarioItemID: 1, requester: &requester.HttpRequester{}, sleeper: &RangeSleep{min: 300, max: 500}, }, { scenarioItemID: 2, requester: &requester.HttpRequester{}, sleeper: &DurationSleep{duration: 1000}, }, { scenarioItemID: 3, requester: &requester.HttpRequester{}, }, }, } // Act service := ScenarioService{} err := service.Init(ctx, scenario, proxies, ScenarioOpts{ Debug: false, IterationCount: 1, MaxConcurrentIterCount: 1, }) // Assert if err != nil { t.Fatalf("TestInitFunc error occurred %v", err) } if err = compareScenarioServiceClients(expectedClients, service.clients); err != nil { t.Fatal(err) } } func TestDo(t *testing.T) { t.Parallel() // Arrange scenario := types.Scenario{ Steps: []types.ScenarioStep{ { ID: 1, Method: types.DefaultMethod, URL: "test.com", Timeout: types.DefaultDuration, }, { ID: 2, Method: types.DefaultMethod, URL: "test.com", Timeout: types.DefaultDuration, }, }, } p1, _ := url.Parse("http://proxy_server.com:80") ctx := context.TODO() mockSleep := &MockSleep{} requesters := []scenarioItemRequester{ { scenarioItemID: 1, sleeper: mockSleep, requester: &MockHttpRequester{ReturnSend: &types.ScenarioStepResult{StepID: 1}}, }, { scenarioItemID: 2, requester: &MockHttpRequester{ReturnSend: &types.ScenarioStepResult{StepID: 2}}, }, } cPool, _ := NewClientPool(1, 1, types.EngineModeDdosify, func() *http.Client { return &http.Client{} }, func(c *http.Client) { c.CloseIdleConnections() }) service := ScenarioService{ clients: map[*url.URL][]scenarioItemRequester{ p1: requesters, }, scenario: scenario, ctx: ctx, cPool: cPool, } expectedResponse := types.ScenarioResult{ ProxyAddr: p1, StepResults: []*types.ScenarioStepResult{ {StepID: 1}, {StepID: 2}, }, } // Act response, err := service.Do(p1, time.Now()) // Assert if err != nil { t.Fatalf("TestDo errored: %v", err) } if response.ProxyAddr != expectedResponse.ProxyAddr { t.Fatalf("[ProxyAddr] Expected %v, Found: %v", expectedResponse.ProxyAddr, response.ProxyAddr) } if !reflect.DeepEqual(expectedResponse.StepResults, response.StepResults) { t.Fatalf("[ResponseItem] Expected %#v, Found: %#v", expectedResponse.StepResults, response.StepResults) } if !mockSleep.SleepCalled { t.Fatalf("[Sleep] Sleep should be called") } if mockSleep.SleepCallCount != 1 { t.Fatalf("[Sleep] Sleep call count expected: %d, Found: %d", 1, mockSleep.SleepCallCount) } } func TestDoErrorOnSend(t *testing.T) { t.Parallel() // Arrange scenario := types.Scenario{ Steps: []types.ScenarioStep{ { ID: 1, Method: types.DefaultMethod, URL: "test.com", Timeout: types.DefaultDuration, }, }, } p1, _ := url.Parse("http://proxy_server.com:80") ctx := context.TODO() requestersProxyError := []scenarioItemRequester{ { scenarioItemID: 1, requester: &MockHttpRequester{ReturnSend: &types.ScenarioStepResult{Err: types.RequestError{Type: types.ErrorProxy}}}, }, } requestersIntentedError := []scenarioItemRequester{ { scenarioItemID: 1, requester: &MockHttpRequester{ReturnSend: &types.ScenarioStepResult{Err: types.RequestError{Type: types.ErrorIntented}}}, }, } requestersConnError := []scenarioItemRequester{ { scenarioItemID: 1, requester: &MockHttpRequester{ReturnSend: &types.ScenarioStepResult{Err: types.RequestError{Type: types.ErrorConn}}}, }, } tests := []struct { name string requesters []scenarioItemRequester shouldErr bool errorType string responseItemsShouldEmpty bool }{ {"ProxyError", requestersProxyError, true, types.ErrorProxy, false}, {"IntentedError", requestersIntentedError, true, types.ErrorIntented, true}, {"ConnError", requestersConnError, false, "", false}, } for _, test := range tests { t.Run(test.name, func(t *testing.T) { cPool, _ := NewClientPool(1, 1, types.EngineModeDdosify, func() *http.Client { return &http.Client{} }, func(c *http.Client) { c.CloseIdleConnections() }) service := ScenarioService{ clients: map[*url.URL][]scenarioItemRequester{ p1: test.requesters, }, scenario: scenario, ctx: ctx, cPool: cPool, } // Act res, err := service.Do(p1, time.Now()) // Assert if test.shouldErr { if err == nil { t.Fatalf("Should be errored") } if err.Type != test.errorType { t.Fatalf("Expected: %v, Found: %v", test.errorType, err.Type) } } else { if err != nil { t.Fatalf("Errored: %v", err) } } if test.responseItemsShouldEmpty && len(res.StepResults) > 0 { t.Fatalf("ResponseItem should be empty: %v", res.StepResults) } if !test.responseItemsShouldEmpty && len(res.StepResults) == 0 { t.Fatal("ResponseItem shouldn't be empty") } }) } } // func TestDoErrorOnNewRequester(t *testing.T) { // t.Parallel() // // Arrange // scenario := types.Scenario{ // Steps: []types.ScenarioStep{ // { // ID: 1, // Method: types.DefaultMethod, // URL: "test.com", // Timeout: types.DefaultDuration, // }, // }, // } // p1, _ := url.Parse("http://proxy_server.com:80") // ctx := context.TODO() // service := ScenarioService{ // clients: map[*url.URL][]scenarioItemRequester{}, // scenario: scenario, // ctx: ctx, // } // // Act // _, err := service.Do(p1, time.Now()) // // Assert // if err == nil { // t.Fatalf("TestDoErrorOnNewRequester should be errored") // } // if err.Type != types.ErrorUnkown { // t.Fatalf("Do should return types.ErrorUnkown error type") // } // } func TestDone(t *testing.T) { t.Parallel() // Arrange scenario := types.Scenario{ Steps: []types.ScenarioStep{ { ID: 1, Method: types.DefaultMethod, URL: "test.com", Timeout: types.DefaultDuration, }, }, } p1, _ := url.Parse("http://proxy_server.com:80") p2, _ := url.Parse("http://proxy_server.com:8080") ctx := context.TODO() cPool, _ := NewClientPool(1, 1, types.EngineModeDdosify, func() *http.Client { return &http.Client{} }, func(c *http.Client) { c.CloseIdleConnections() }) requester1 := &MockHttpRequester{ReturnSend: &types.ScenarioStepResult{StepID: 1}} requester2 := &MockHttpRequester{ReturnSend: &types.ScenarioStepResult{StepID: 2}} requester3 := &MockHttpRequester{ReturnSend: &types.ScenarioStepResult{StepID: 1}} requester4 := &MockHttpRequester{ReturnSend: &types.ScenarioStepResult{StepID: 2}} service := ScenarioService{ clients: map[*url.URL][]scenarioItemRequester{ p1: { { scenarioItemID: 1, requester: requester1, }, { scenarioItemID: 2, requester: requester2, }, }, p2: { { scenarioItemID: 1, requester: requester3, }, { scenarioItemID: 2, requester: requester4, }, }, }, scenario: scenario, ctx: ctx, cPool: cPool, } // Act service.Done() // Assert if !requester1.DoneCalled { t.Fatalf("Requester1 Done should be called") } if !requester2.DoneCalled { t.Fatalf("Requester2 Done should be called") } if !requester3.DoneCalled { t.Fatalf("Requester3 Done should be called") } if !requester4.DoneCalled { t.Fatalf("Requester4 Done should be called") } } func TestGetOrCreateRequesters(t *testing.T) { t.Parallel() // Arrange scenario := types.Scenario{ Steps: []types.ScenarioStep{ { ID: 1, Method: types.DefaultMethod, URL: "test.com", Timeout: types.DefaultDuration, }, }, } p1, _ := url.Parse("http://proxy_server.com:80") proxies := []*url.URL{p1} ctx := context.TODO() service := ScenarioService{} service.Init(ctx, scenario, proxies, ScenarioOpts{ Debug: false, IterationCount: 1, MaxConcurrentIterCount: 1, }) expectedRequesters := []scenarioItemRequester{{scenarioItemID: 1, requester: &requester.HttpRequester{}}} expectedClients := map[*url.URL][]scenarioItemRequester{ p1: expectedRequesters, } // Act requesters, err := service.getOrCreateRequesters(p1) // Assert if err != nil { t.Fatalf("TestGetOrCreateRequesters errored: %v", err) } if len(expectedRequesters) != len(requesters) || expectedRequesters[0].scenarioItemID != requesters[0].scenarioItemID || reflect.TypeOf(expectedRequesters[0].requester) != reflect.TypeOf(requesters[0].requester) { t.Fatalf("Expected: %v, Found: %v", expectedRequesters, requesters) } if err = compareScenarioServiceClients(expectedClients, service.clients); err != nil { t.Fatal(err) } } func TestGetOrCreateRequestersNewProxy(t *testing.T) { t.Parallel() // Arrange scenario := types.Scenario{ Steps: []types.ScenarioStep{ { ID: 1, Method: types.DefaultMethod, URL: "test.com", Timeout: types.DefaultDuration, }, }, } p1, _ := url.Parse("http://proxy_server.com:80") proxies := []*url.URL{p1} ctx := context.TODO() service := ScenarioService{} service.Init(ctx, scenario, proxies, ScenarioOpts{ Debug: false, IterationCount: 1, MaxConcurrentIterCount: 1, }) expectedRequesters := []scenarioItemRequester{{scenarioItemID: 1, requester: &requester.HttpRequester{}}} p2, _ := url.Parse("http://proxy_server2.com:8080") expectedClients := map[*url.URL][]scenarioItemRequester{ p1: {{scenarioItemID: 1, requester: &requester.HttpRequester{}}}, p2: {{scenarioItemID: 1, requester: &requester.HttpRequester{}}}, } // Act requesters, err := service.getOrCreateRequesters(p2) // Assert if err != nil { t.Fatalf("TestGetOrCreateRequestersNewProxy errored: %v", err) } if len(expectedRequesters) != len(requesters) || expectedRequesters[0].scenarioItemID != requesters[0].scenarioItemID || reflect.TypeOf(expectedRequesters[0].requester) != reflect.TypeOf(requesters[0].requester) { t.Fatalf("Expected: %v, Found: %v", expectedRequesters, requesters) } if err = compareScenarioServiceClients(expectedClients, service.clients); err != nil { t.Fatal(err) } } func TestCreateRequestersErrorOnRequesterInit(t *testing.T) { t.Parallel() // Arrange scenario := types.Scenario{ Steps: []types.ScenarioStep{ { ID: 1, Method: "?", // To fail HttpRequesters.Init method URL: "test.com", Timeout: types.DefaultDuration, }, }, } p, _ := url.Parse("http://proxy_server.com:80") ctx := context.TODO() service := ScenarioService{ clients: map[*url.URL][]scenarioItemRequester{}, scenario: scenario, ctx: ctx, } // Act err := service.createRequesters(p) // Assert if err == nil { t.Fatal("TestCreateRequestersFailOnNewRequester should be errored") } } func TestnewSleeper(t *testing.T) { t.Parallel() sleepRange := "300-500" sleepRangeReverse := "500-300" sleepDuration := "1000" expectedSleepRange := &RangeSleep{ min: 300, max: 500, } exptectedSleepDuration := &DurationSleep{ duration: 1000, } // "range" sleep strategy test sleep := newSleeper(sleepRange) if !reflect.DeepEqual(sleep, expectedSleepRange) { t.Errorf("Expected %v, Found: %v", expectedSleepRange, sleep) } sleep = newSleeper(sleepRangeReverse) if !reflect.DeepEqual(sleep, expectedSleepRange) { t.Errorf("Expected %v, Found: %v", expectedSleepRange, sleep) } // "duration" sleep strategy test sleep = newSleeper(sleepDuration) if !reflect.DeepEqual(sleep, exptectedSleepDuration) { t.Errorf("Expected %v, Found: %v", exptectedSleepDuration, sleep) } } func TestSleep(t *testing.T) { t.Parallel() delta := time.Duration(100) min := 300 max := 500 dur := 1000 if testing.Short() { // Arrange durations for poor machines delta = time.Duration(600) min = 750 max = 1250 dur = 1000 } sleepDuration := &DurationSleep{ duration: dur, } sleepRange := &RangeSleep{ min: min, max: max, } // Test range start := time.Now() sleepRange.sleep() elapsed := time.Duration(time.Since(start) / time.Millisecond) if elapsed > time.Duration(max)+delta || elapsed < time.Duration(min)-delta { t.Errorf("Expected: [%d-%d], Found: %d", min, max, elapsed) } // Test exact duration start = time.Now() sleepDuration.sleep() elapsed = time.Duration(time.Since(start) / time.Millisecond) if elapsed > time.Duration(dur)+delta { t.Errorf("Expected: %d, Found: %d", dur, elapsed) } } func TestInjectDynamicVars(t *testing.T) { invalidDynamicKey := "{{_randomDdppdd}}" envs := map[string]interface{}{ "country": "{{_randomCountry}}", "X": "Y", "{{xx}}": "xx", "notFoundDynamicKey": invalidDynamicKey, } beforeLen := len(envs) vi := &injection.EnvironmentInjector{} vi.Init() injectDynamicVars(vi, envs) afterLen := len(envs) if beforeLen != afterLen { t.Errorf("number of envs changed during dynamic var injection") } if val, ok := envs["country"]; !ok || val == "{{_randomCountry}}" { t.Errorf("injection failure") } if val, ok := envs["notFoundDynamicKey"]; !ok || val != invalidDynamicKey { t.Errorf("not found key should stay same") } } func TestOnlyOneClientInDebugModeInUserMode(t *testing.T) { t.Parallel() // Arrange scenario := types.Scenario{ Steps: []types.ScenarioStep{ { ID: 1, Method: types.DefaultMethod, URL: "test.com", Timeout: types.DefaultDuration, }, }, } p1, _ := url.Parse("http://proxy_server.com:80") proxies := []*url.URL{p1} ctx := context.TODO() service := ScenarioService{} service.Init(ctx, scenario, proxies, ScenarioOpts{ Debug: true, EngineMode: types.EngineModeDistinctUser, IterationCount: 100, MaxConcurrentIterCount: 5, }) if service.cPool.Len() != 1 { t.Fatal("TestOnlyOneClientInDebugModeInUserMode should have only one client") } } ================================================ FILE: ddosify_engine/core/types/error.go ================================================ /* * * Ddosify - Load testing tool for any web system. * Copyright (C) 2021 Ddosify (https://ddosify.com) * * This program is free software: you can redistribute it and/or modify * it under the terms of the GNU Affero General Public License as published * by the Free Software Foundation, either version 3 of the License, or * (at your option) any later version. * * This program is distributed in the hope that it will be useful, * but WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * GNU Affero General Public License for more details. * * You should have received a copy of the GNU Affero General Public License * along with this program. If not, see . * */ package types import "fmt" // Constants for custom error types and reasons const ( // Types ErrorProxy = "proxyError" ErrorConn = "connectionError" ErrorUnkown = "unknownError" ErrorIntented = "intentedError" // Errors for created intentionally ErrorDns = "dnsError" ErrorParse = "parseError" ErrorAddr = "addressError" ErrorInvalidRequest = "invalidRequestError" // Reasons ReasonProxyFailed = "proxy connection refused" ReasonProxyTimeout = "proxy timeout" ReasonConnTimeout = "connection timeout" ReasonReadTimeout = "read timeout" ReasonConnRefused = "connection refused" // In gracefully stop, engine cancels the ongoing requests. // We can detect the canceled requests with the help of this. ReasonCtxCanceled = "context canceled" ) // RequestError is our custom error struct created in the requester.Requester implementations. type RequestError struct { Type string Reason string } // Custom error message method of ScenarioError func (e *RequestError) Error() string { return fmt.Sprintf("%s: %s", e.Type, e.Reason) } type ScenarioValidationError struct { // UnWrappable msg string wrappedErr error } func (sc ScenarioValidationError) Error() string { return sc.msg } func (sc ScenarioValidationError) Unwrap() error { return sc.wrappedErr } type EnvironmentNotDefinedError struct { // UnWrappable msg string wrappedErr error } func (sc EnvironmentNotDefinedError) Error() string { return sc.msg } func (sc EnvironmentNotDefinedError) Unwrap() error { return sc.wrappedErr } type CaptureConfigError struct { // UnWrappable msg string wrappedErr error } func (sc CaptureConfigError) Error() string { return sc.msg } func (sc CaptureConfigError) Unwrap() error { return sc.wrappedErr } type FailedAssertion struct { Rule string Received map[string]interface{} Reason string } ================================================ FILE: ddosify_engine/core/types/hammer.go ================================================ /* * * Ddosify - Load testing tool for any web system. * Copyright (C) 2021 Ddosify (https://ddosify.com) * * This program is free software: you can redistribute it and/or modify * it under the terms of the GNU Affero General Public License as published * by the Free Software Foundation, either version 3 of the License, or * (at your option) any later version. * * This program is distributed in the hope that it will be useful, * but WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * GNU Affero General Public License for more details. * * You should have received a copy of the GNU Affero General Public License * along with this program. If not, see . * */ package types import ( "fmt" "net/http" "strings" "go.ddosify.com/ddosify/core/proxy" "go.ddosify.com/ddosify/core/util" ) // Constants for Hammer field values const ( // Constants of the Load Types LoadTypeLinear = "linear" LoadTypeIncremental = "incremental" LoadTypeWaved = "waved" // EngineModes EngineModeDistinctUser = "distinct-user" EngineModeRepeatedUser = "repeated-user" EngineModeDdosify = "ddosify" // Default Values DefaultIterCount = 100 DefaultLoadType = LoadTypeLinear DefaultDuration = 10 DefaultTimeout = 5 DefaultMethod = http.MethodGet DefaultOutputType = "stdout" // TODO: get this value from report.OutputTypeStdout when import cycle resolved. DefaultSamplingCount = 3 DefaultSingleMode = true ) var loadTypes = [...]string{LoadTypeLinear, LoadTypeIncremental, LoadTypeWaved} var engineModes = [...]string{EngineModeDdosify, EngineModeDistinctUser, EngineModeRepeatedUser} type TestAssertionOpt struct { Abort bool Delay int } // TimeRunCount is the data structure to store manual load type data. type TimeRunCount []struct { Duration int Count int } type Tag struct { Tag string `json:"tag"` Type string `json:"type"` } type CsvConf struct { Path string `json:"path"` Delimiter string `json:"delimiter"` SkipFirstLine bool `json:"skip_first_line"` Vars map[string]Tag `json:"vars"` // "0":"name", "1":"city","2":"team" SkipEmptyLine bool `json:"skip_empty_line"` AllowQuota bool `json:"allow_quota"` Order string `json:"order"` } // TimeRunCount is the data structure to store manual load type data. type CustomCookie struct { Name string `json:"name"` Value string `json:"value"` Domain string `json:"domain"` Path string `json:"path"` Expires string `json:"expires"` MaxAge int `json:"max_age"` HttpOnly bool `json:"http_only"` Secure bool `json:"secure"` Raw string `json:"raw"` } // Hammer is like a lighter for the engine. // It includes attack metadata and all necessary data to initialize the internal services in the engine. type Hammer struct { // Total iteration count IterationCount int // Type of the load. LoadType string // Total Duration of the test in seconds. TestDuration int // Duration (in second) - Request count map. Example: {10: 1500, 50: 400, ...} TimeRunCountMap TimeRunCount // Test Scenario Scenario Scenario // Proxy/Proxies to use Proxy proxy.Proxy // Destination of the results data. ReportDestination string // Dynamic field for extra parameters. Others map[string]interface{} // Debug mode on/off Debug bool // Sampling rate SamplingRate int // Connection reuse EngineMode string // Test Data Config TestDataConf map[string]CsvConf // Custom Cookies Cookies []CustomCookie // Custom Cookies Enabled CookiesEnabled bool // Test-wide assertions Assertions map[string]TestAssertionOpt // Engine runs single SingleMode bool } // Validate validates attack metadata and executes the validation methods of the services. func (h *Hammer) Validate() error { if len(h.Scenario.Steps) == 0 { return fmt.Errorf("scenario or target is empty") } h.Scenario.CsvVars = getCsvEnvs(h.TestDataConf) if err := h.Scenario.validate(); err != nil { return err } if h.LoadType != "" && !util.StringInSlice(h.LoadType, loadTypes[:]) { return fmt.Errorf("unsupported LoadType: %s", h.LoadType) } if h.EngineMode != "" && !util.StringInSlice(h.EngineMode, engineModes[:]) { return fmt.Errorf("unsupported EngineMode: %s", h.EngineMode) } if len(h.TimeRunCountMap) > 0 { for _, t := range h.TimeRunCountMap { if t.Duration < 1 { return fmt.Errorf("duration in manual_load should be greater than 0") } } } return nil } func getCsvEnvs(testDataConf map[string]CsvConf) []string { csvVars := make([]string, 0) sb := strings.Builder{} for key, conf := range testDataConf { for _, tag := range conf.Vars { sb.WriteString("data.") sb.WriteString(key) sb.WriteString(".") sb.WriteString(tag.Tag) // data.info.name csvVars = append(csvVars, sb.String()) sb.Reset() } } return csvVars } ================================================ FILE: ddosify_engine/core/types/hammer_test.go ================================================ /* * * Ddosify - Load testing tool for any web system. * Copyright (C) 2021 Ddosify (https://ddosify.com) * * This program is free software: you can redistribute it and/or modify * it under the terms of the GNU Affero General Public License as published * by the Free Software Foundation, either version 3 of the License, or * (at your option) any later version. * * This program is distributed in the hope that it will be useful, * but WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * GNU Affero General Public License for more details. * * You should have received a copy of the GNU Affero General Public License * along with this program. If not, see . * */ package types import ( "errors" "testing" "go.ddosify.com/ddosify/core/proxy" ) func newDummyHammer() Hammer { return Hammer{ Proxy: proxy.Proxy{Strategy: proxy.ProxyTypeSingle}, ReportDestination: DefaultOutputType, Scenario: Scenario{ Steps: []ScenarioStep{ { ID: 1, Method: "GET", URL: "http://127.0.0.1", }, }, }, } } func TestHammerValidAttackType(t *testing.T) { var loadTypes = [...]string{"linear", "incremental", "waved"} for _, l := range loadTypes { h := newDummyHammer() h.LoadType = l if err := h.Validate(); err != nil { t.Errorf("TestHammerValidAttackType errored: %v", err) } } } func TestHammerInValidAttackType(t *testing.T) { h := newDummyHammer() h.LoadType = "strees" if err := h.Validate(); err == nil { t.Errorf("TestHammerInValidAttackType errored") } } func TestHammerValidAuth(t *testing.T) { for _, v := range supportedAuthentications { h := newDummyHammer() h.Scenario.Steps[0].Auth = Auth{ Type: v, Username: "test", Password: "123", } if err := h.Validate(); err != nil { t.Errorf("TestHammerValidAuth errored: %v", err) } } } func TestHammerInValidAuth(t *testing.T) { h := newDummyHammer() h.Scenario.Steps[0].Auth = Auth{ Type: "invalidAuthType", Username: "test", Password: "123", } if err := h.Validate(); err == nil { t.Errorf("TestHammerInValidReportDestination errored") } } func TestHammerValidScenario(t *testing.T) { // Single Scenario for _, m := range supportedProtocolMethods { h := newDummyHammer() h.Scenario = Scenario{ Steps: []ScenarioStep{ { ID: 1, Method: m, URL: "https://127.0.0.1", }, }, } if err := h.Validate(); err != nil { t.Errorf("TestHammerValidScenario single scenario errored: %v", err) } } // Multiple Scenario for _, m := range supportedProtocolMethods { h := newDummyHammer() h.Scenario = Scenario{ Steps: []ScenarioStep{ { ID: 1, Method: m, URL: "https://127.0.0.1", }, { ID: 2, URL: "https://127.0.0.1", Method: m, }, }, } if err := h.Validate(); err != nil { t.Errorf("TestHammerValidScenario multi scenario errored: %v", err) } } } func TestHammerEmptyScenario(t *testing.T) { h := newDummyHammer() h.Scenario = Scenario{} if err := h.Validate(); err == nil { t.Errorf("TestHammerEmptyScenario errored") } } func TestHammerInvalidScenarioMethod(t *testing.T) { // Single Scenario h := newDummyHammer() h.Scenario = Scenario{ Steps: []ScenarioStep{ { ID: 1, Method: "GETT", }, }, } if err := h.Validate(); err == nil { t.Errorf("TestHammerInvalidScenarioMethod errored") } // Multi Scenario h = newDummyHammer() h.Scenario = Scenario{ Steps: []ScenarioStep{ { ID: 1, Method: supportedProtocolMethods[1], }, { ID: 1, Method: "GETT", }, }, } if err := h.Validate(); err == nil { t.Errorf("TestHammerInvalidScenarioMethod errored") } } func TestHammerEmptyScenarioStepID(t *testing.T) { // Single Scenario h := newDummyHammer() h.Scenario = Scenario{ Steps: []ScenarioStep{ { Method: supportedProtocolMethods[1], }, }, } if err := h.Validate(); err == nil { t.Errorf("1- TestHammerEmptyScenarioStepID should be errored") } // Multi Scenario h = newDummyHammer() h.Scenario = Scenario{ Steps: []ScenarioStep{ { ID: 1, Method: supportedProtocolMethods[1], }, { Method: supportedProtocolMethods[1], }, }, } if err := h.Validate(); err == nil { t.Errorf("2- TestHammerEmptyScenarioStepID should be errored") } } func TestHammerDuplicateScenarioStepID(t *testing.T) { // Single Scenario h := newDummyHammer() h.Scenario = Scenario{ Steps: []ScenarioStep{ { ID: 1, Method: supportedProtocolMethods[1], }, { ID: 2, Method: supportedProtocolMethods[1], }, { ID: 2, Method: supportedProtocolMethods[1], }, }, } if err := h.Validate(); err == nil { t.Errorf("TestHammerDuplicateScenarioStepID should be errored") } } func TestHammerStepSleep(t *testing.T) { t.Parallel() invalidSleeps := []string{ "-300", "-300-500", "300s", "as", "100000", // More than maxSleep } validSleeps := []string{ "300-500", "1000", } tests := []struct { name string sleep string shouldErr bool }{ {"Invalid 1", invalidSleeps[0], true}, {"Invalid 2", invalidSleeps[1], true}, {"Invalid 3", invalidSleeps[2], true}, {"Invalid 4", invalidSleeps[3], true}, {"Invalid 5", invalidSleeps[4], true}, {"ValidRange", validSleeps[0], false}, {"ValidDuration", validSleeps[1], false}, } for _, tc := range tests { test := tc t.Run(test.name, func(t *testing.T) { t.Parallel() h := newDummyHammer() h.Scenario = Scenario{ Steps: []ScenarioStep{ { ID: 1, URL: "target.com", Method: supportedProtocolMethods[1], Sleep: test.sleep, }, }, } err := h.Validate() if test.shouldErr { if err == nil { t.Errorf("Should be errored") } } else { if err != nil { t.Errorf("Error occurred %v", err) } } }) } } func TestHammerInvalidManualLoadDuration(t *testing.T) { // Duration = 0 h := newDummyHammer() h.TimeRunCountMap = TimeRunCount{ {Duration: 10, Count: 10}, {Duration: 0, Count: 10}, } if err := h.Validate(); err == nil { t.Errorf("TestHammerInvalidManualLoadDuration errored") } // Duration is negatie h = newDummyHammer() h.TimeRunCountMap = TimeRunCount{ {Duration: 10, Count: 10}, {Duration: -1, Count: 10}, } if err := h.Validate(); err == nil { t.Errorf("TestHammerInvalidManualLoadDuration errored") } } func TestHammerAccessingNotDefinedCsvEnvs(t *testing.T) { h := newDummyHammer() h.TestDataConf = make(map[string]CsvConf) h.TestDataConf["info"] = CsvConf{ Path: "", Delimiter: "", SkipFirstLine: false, Vars: map[string]Tag{ "0": { Tag: "a", Type: "string", }, }, SkipEmptyLine: false, AllowQuota: false, Order: "", } h.Scenario.Steps = []ScenarioStep{ { ID: 1, Name: "x", Method: "GET", Headers: map[string]string{ "{{data.info.x}}": "X", }, Payload: "", URL: "https://ddosify.com", }, } err := h.Validate() var environmentNotDefined EnvironmentNotDefinedError if !errors.As(err, &environmentNotDefined) { t.Errorf("Should be EnvironmentNotDefinedError") } } ================================================ FILE: ddosify_engine/core/types/regex/regex.go ================================================ package regex const DynamicVariableRegex = `\{{(_)[^}]+\}}` const JsonDynamicVariableRegex = `\"{{(_)[^}]+\}}"` const EnvironmentVariableRegex = `{{[a-zA-Z$][a-zA-Z0-9_().-]*}}` const JsonEnvironmentVarRegex = `\"{{[a-zA-Z$][a-zA-Z0-9_().-]*}}"` ================================================ FILE: ddosify_engine/core/types/regex/regex_test.go ================================================ package regex import ( "regexp" "testing" ) func TestDynamicVariableRegex(t *testing.T) { re := regexp.MustCompile(DynamicVariableRegex) // Sub Tests tests := []struct { name string url string shouldMatch bool }{ {"Match1", "https://example.com/{{_abc}}", true}, {"Match2", "https://example.com/{{_timestamp}}", true}, {"Match3", "https://example.com/aaa/{{_timestamp}}", true}, {"Match4", "https://example.com/aaa/{{_timestamp}}/bbb", true}, {"Match5", "https://example.com/{{_timestamp}}/{_abc}", true}, {"Match6", "https://example.com/{{_abc/{{_timestamp}}", true}, {"Match7", "https://example.com/_aaa/{{_timestamp}}", true}, {"Not Match1", "https://example.com/{{_abc", false}, {"Not Match2", "https://example.com/{{_abc}", false}, {"Not Match3", "https://example.com/_abc", false}, {"Not Match4", "https://example.com/{{abc", false}, {"Not Match5", "https://example.com/abc", false}, {"Not Match6", "https://example.com/abc/{{cc}}", false}, {"Not Match7", "https://example.com/abc/{{cc}}/fcf", false}, } for _, test := range tests { tf := func(t *testing.T) { matched := re.MatchString(test.url) if test.shouldMatch != matched { t.Errorf("Name: %s, ShouldMatch: %t, Matched: %t\n", test.name, test.shouldMatch, matched) } } t.Run(test.name, tf) } } func TestEnvironmentVariableRegex(t *testing.T) { re := regexp.MustCompile(EnvironmentVariableRegex) // Sub Tests tests := []struct { name string url string shouldMatch bool }{ {"Match1", "{{a}}", true}, {"Match2", "{{ab}}", true}, {"Match3", "{{ab1}}", true}, {"Match4", "{{Ab1}}/bbb", true}, {"Match5", "{{ABC}}/{_abc}", true}, {"Match6", "{{_abc/{{ABC__fc_111}}", true}, {"Match7", "{{a_b}}", true}, {"Match8", "xx{{a}}", true}, {"Match9", "{{a}}bb", true}, {"Match10", "cx{{a}}vc", true}, {"Match10", "cx {{a}}vc", true}, {"Match11", "cx{{a}} vc", true}, {"Match11", "cx{{a}}_-", true}, {"Match12", "{{a-v}}", true}, {"Match13", "{{AV-}}", true}, {"Not Match1", "{{}}", false}, {"Not Match2", "{{_abc}}", false}, {"Not Match4", "{{abc!}}", false}, {"Not Match6", "{{_A}}", false}, {"Not Match7", "{{_AB_2}}", false}, {"Not Match8", "{{£AB_2}}", false}, {"Not Match8", "{{3AB_2}}", false}, {"Not Match8", "{{%3AB_2}}", false}, } for _, test := range tests { tf := func(t *testing.T) { matched := re.MatchString(test.url) if test.shouldMatch != matched { t.Errorf("Name: %s, ShouldMatch: %t, Matched: %t\n", test.name, test.shouldMatch, matched) } } t.Run(test.name, tf) } } ================================================ FILE: ddosify_engine/core/types/response.go ================================================ /* * * Ddosify - Load testing tool for any web system. * Copyright (C) 2021 Ddosify (https://ddosify.com) * * This program is free software: you can redistribute it and/or modify * it under the terms of the GNU Affero General Public License as published * by the Free Software Foundation, either version 3 of the License, or * (at your option) any later version. * * This program is distributed in the hope that it will be useful, * but WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * GNU Affero General Public License for more details. * * You should have received a copy of the GNU Affero General Public License * along with this program. If not, see . * */ package types import ( "net/http" "net/url" "time" "github.com/google/uuid" ) // ScenarioResult is corresponding to Scenario. Each Scenario has a ScenarioResult after the scenario is played. type ScenarioResult struct { // First request start time for the Scenario StartTime time.Time ProxyAddr *url.URL StepResults []*ScenarioStepResult // Dynamic field for extra data needs in response object consumers. Others map[string]interface{} } // ScenarioStepResult is corresponding to ScenarioStep. type ScenarioStepResult struct { // ID of the ScenarioStep StepID uint16 // Name of the ScenarioStep StepName string // Each request has a unique ID. RequestID uuid.UUID // Returned status code. Has different meaning for different protocols. StatusCode int // Time of the request call. RequestTime time.Time // Total duration. From request sending to full response receiving. Duration time.Duration // Response content length ContentLength int64 // Error occurred at request time. Err RequestError // Url Url string // Method Method string // Request Headers ReqHeaders http.Header // Request Body ReqBody []byte // Response Headers RespHeaders http.Header // Response Body RespBody []byte // Protocol specific metrics. For ex: DNSLookupDuration: 1s for HTTP Custom map[string]interface{} // Usable envs in this step UsableEnvs map[string]interface{} // Captured envs in this step ExtractedEnvs map[string]interface{} // Failed captures and their reasons FailedCaptures map[string]string // Failed assertion rules and received values FailedAssertions []FailedAssertion } ================================================ FILE: ddosify_engine/core/types/scenario.go ================================================ /* * * Ddosify - Load testing tool for any web system. * Copyright (C) 2021 Ddosify (https://ddosify.com) * * This program is free software: you can redistribute it and/or modify * it under the terms of the GNU Affero General Public License as published * by the Free Software Foundation, either version 3 of the License, or * (at your option) any later version. * * This program is distributed in the hope that it will be useful, * but WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * GNU Affero General Public License for more details. * * You should have received a copy of the GNU Affero General Public License * along with this program. If not, see . * */ package types import ( "crypto/tls" "crypto/x509" "fmt" "io/ioutil" "net/http" "os" "regexp" "strconv" "strings" validator "github.com/asaskevich/govalidator" "go.ddosify.com/ddosify/core/util" ) // Constants for Scenario field values const ( // Constants of the Protocol types ProtocolHTTP = "HTTP" ProtocolHTTPS = "HTTPS" // Constants of the Auth types AuthHttpBasic = "basic" // Max sleep in ms (90s) maxSleep = 90000 // Should match environment variables, reference EnvironmentVariableRegexStr = `{{[a-zA-Z$][a-zA-Z0-9_().-]*}}` // Should match environment variables, definition, exact match EnvironmentVariableNameStr = `^[a-zA-Z][a-zA-Z0-9_-]*$` ) // SupportedProtocols should be updated whenever a new requester.Requester interface implemented var SupportedProtocols = [...]string{ProtocolHTTP, ProtocolHTTPS} var supportedProtocolMethods = []string{ http.MethodGet, http.MethodPost, http.MethodPut, http.MethodDelete, http.MethodPatch, http.MethodHead, http.MethodOptions, } var supportedAuthentications = []string{ AuthHttpBasic, } var envVarRegexp *regexp.Regexp var envVarNameRegexp *regexp.Regexp func init() { envVarRegexp = regexp.MustCompile(EnvironmentVariableRegexStr) envVarNameRegexp = regexp.MustCompile(EnvironmentVariableNameStr) } // Scenario struct contains a list of ScenarioStep so scenario.ScenarioService can execute the scenario step by step. type Scenario struct { Steps []ScenarioStep Envs map[string]interface{} CsvVars []string // only for validation Data map[string]CsvData // populated data } func (s *Scenario) validate() error { stepIds := make(map[uint16]struct{}, len(s.Steps)) definedEnvs := map[string]struct{}{} // add global envs for key := range s.Envs { if !envVarNameRegexp.MatchString(key) { // not a valid env definition return fmt.Errorf("env key is not valid: %s", key) } definedEnvs[key] = struct{}{} // exist } // add csv vars for _, key := range s.CsvVars { // data.info.name splitted := strings.Split(key, ".") if len(splitted) > 3 { return fmt.Errorf("csv key can not have dot in it: %s", key) } for _, s := range splitted { if !envVarNameRegexp.MatchString(s) { // not a valid env definition return fmt.Errorf("csv key is not valid: %s", key) } } definedEnvs[key] = struct{}{} // exist } for _, st := range s.Steps { if err := st.validate(definedEnvs); err != nil { return err } // enrich Envs map with captured envs from each step for _, ce := range st.EnvsToCapture { if !envVarNameRegexp.MatchString(ce.Name) { // not a valid env definition return fmt.Errorf("captured env key is not valid: %s", ce.Name) } definedEnvs[ce.Name] = struct{}{} } if _, ok := stepIds[st.ID]; ok { return fmt.Errorf("duplicate step id: %d", st.ID) } stepIds[st.ID] = struct{}{} } return nil } func checkEnvsValidInStep(st *ScenarioStep, definedEnvs map[string]struct{}) error { var err error matchInEnvs := func(matches []string) error { for _, v := range matches { if _, ok := definedEnvs[v[2:len(v)-2]]; !ok { // {{....}} // utility functions are matched too, check if starts with rand // TODO: find a better solution about utility functions and validation checks if strings.HasPrefix(v[2:len(v)-2], "rand(") { if _, ok := definedEnvs[v[7:len(v)-3]]; ok { continue } } if strings.HasPrefix(v[2:len(v)-2], "$") { varName := v[3 : len(v)-2] if _, ok := os.LookupEnv(varName); ok { continue } return EnvironmentNotDefinedError{ msg: fmt.Sprintf("%s is not found in the operating system environment variables", v), } } return EnvironmentNotDefinedError{ msg: fmt.Sprintf("%s is not defined to use by global and captured environments", v), } } } return nil } f := func(source string) error { matches := envVarRegexp.FindAllString(source, -1) return matchInEnvs(matches) } // check env usage in url err = f(st.URL) if err != nil { return err } // check env usage in header for k, v := range st.Headers { err = f(k) if err != nil { return err } err = f(v) if err != nil { return err } } // check env usage in payload err = f(st.Payload) return err } // ScenarioStep represents one step of a Scenario. // This struct should be able to include all necessary data in a network packet for SupportedProtocols. type ScenarioStep struct { // ID of the Item. Should be given by the client. ID uint16 // Name of the Item. Name string // Request Method Method string // Authentication Auth Auth // A TLS cert Cert tls.Certificate // A TLS cert pool CertPool *x509.CertPool // Request Headers Headers map[string]string // Request payload Payload string // Target URL URL string // Connection timeout duration of the request in seconds Timeout int // Sleep duration after running the step. Can be a time range like "300-500" or an exact duration like "350" in ms Sleep string // Protocol specific request parameters. For ex: DisableRedirects:true for Http requests Custom map[string]interface{} // Envs to capture from response of this step EnvsToCapture []EnvCaptureConf // assertion expressions Assertions []string } type SourceType string const ( Header SourceType = "header" Body SourceType = "body" Cookie SourceType = "cookies" ) type RegexCaptureConf struct { Exp *string `json:"exp"` No int `json:"matchNo"` } type EnvCaptureConf struct { JsonPath *string `json:"json_path"` Xpath *string `json:"xpath"` XpathHtml *string `json:"xpath_html"` RegExp *RegexCaptureConf `json:"regexp"` Name string `json:"as"` From SourceType `json:"from"` Key *string `json:"header_key"` CookieName *string `json:"cookie_name"` } type CsvData struct { Rows []map[string]interface{} Random bool } // Auth struct should be able to include all necessary authentication realated data for supportedAuthentications. type Auth struct { Type string Username string Password string } func (si *ScenarioStep) validate(definedEnvs map[string]struct{}) error { if !util.StringInSlice(si.Method, supportedProtocolMethods) { return fmt.Errorf("unsupported Request Method: %s", si.Method) } if si.Auth != (Auth{}) && !util.StringInSlice(si.Auth.Type, supportedAuthentications) { return fmt.Errorf("unsupported Authentication Method (%s) ", si.Auth.Type) } if si.ID == 0 { return fmt.Errorf("step ID should be greater than zero") } if !envVarRegexp.MatchString(si.URL) && !validator.IsURL(strings.ReplaceAll(si.URL, " ", "_")) { return fmt.Errorf("target is not valid: %s", si.URL) } if si.Sleep != "" { sleep := strings.Split(si.Sleep, "-") // Avoid invalid syntax like "-300-500" if len(sleep) > 2 { return fmt.Errorf("sleep expression is not valid: %s", si.Sleep) } // Validate string to int conversion for _, s := range sleep { dur, err := strconv.Atoi(s) if err != nil { return fmt.Errorf("sleep is not valid: %s", si.Sleep) } if dur > maxSleep { return fmt.Errorf("maximum sleep limit exceeded. provided: %d ms, maximum: %d ms", dur, maxSleep) } } } for _, conf := range si.EnvsToCapture { err := validateCaptureConf(conf) if err != nil { return wrapAsScenarioValidationError(err) } } // check if referred envs in current step has already been defined or not if err := checkEnvsValidInStep(si, definedEnvs); err != nil { return wrapAsScenarioValidationError(err) } return nil } func wrapAsScenarioValidationError(err error) ScenarioValidationError { return ScenarioValidationError{ msg: fmt.Sprintf("ScenarioValidationError %v", err), wrappedErr: err, } } func validateCaptureConf(conf EnvCaptureConf) error { if !(conf.From == Header || conf.From == Body || conf.From == Cookie) { return CaptureConfigError{ msg: fmt.Sprintf("invalid \"from\" type in capture env : %s", conf.From), } } if conf.From == Header && conf.Key == nil { return CaptureConfigError{ msg: fmt.Sprintf("%s, header key must be specified", conf.Name), } } if conf.From == Body && conf.JsonPath == nil && conf.RegExp == nil && conf.Xpath == nil && conf.XpathHtml == nil { return CaptureConfigError{ msg: fmt.Sprintf("%s, one of json_path, regexp, xpath or xpath_html key must be specified when extracting from body", conf.Name), } } return nil } func ParseTLS(certFile, keyFile string) (tls.Certificate, *x509.CertPool, error) { if certFile == "" || keyFile == "" { return tls.Certificate{}, nil, nil } // Read the key pair to create certificate cert, err := tls.LoadX509KeyPair(certFile, keyFile) if err != nil { return tls.Certificate{}, nil, err } // Create a CA certificate pool and add cert.pem to it caCert, err := ioutil.ReadFile(certFile) if err != nil { return tls.Certificate{}, nil, err } pool := x509.NewCertPool() pool.AppendCertsFromPEM(caCert) return cert, pool, nil } func IsTargetValid(url string) error { if !envVarRegexp.MatchString(url) && !validator.IsURL(strings.ReplaceAll(url, " ", "_")) { return fmt.Errorf("target is not valid: %s", url) } return nil } ================================================ FILE: ddosify_engine/core/types/scenario_test.go ================================================ package types import ( "crypto/tls" "crypto/x509" "errors" "net/http" "testing" ) func TestScenarioStepValid_EnvVariableInHeader(t *testing.T) { url := "https://test.com" st := ScenarioStep{ ID: 22, Name: "", Method: http.MethodGet, Auth: Auth{}, Cert: tls.Certificate{}, CertPool: &x509.CertPool{}, Headers: map[string]string{ "{{ARGENTINA}}": "{{ARGENTINA}}", }, Payload: "", URL: url, Timeout: 0, Sleep: "", Custom: map[string]interface{}{}, EnvsToCapture: []EnvCaptureConf{}, } definedEnvs := map[string]struct{}{} err := st.validate(definedEnvs) var environmentNotDefined EnvironmentNotDefinedError if !errors.As(err, &environmentNotDefined) { t.Errorf("Should be EnvironmentNotDefinedError") } t.Logf("%v", environmentNotDefined) } func TestScenarioStepValid_EnvVariableInPayload(t *testing.T) { url := "https://test.com" st := ScenarioStep{ ID: 22, Name: "", Method: http.MethodGet, Auth: Auth{}, Cert: tls.Certificate{}, CertPool: &x509.CertPool{}, Headers: map[string]string{}, Payload: "{{ARGENTINA}}", URL: url, Timeout: 0, Sleep: "", Custom: map[string]interface{}{}, EnvsToCapture: []EnvCaptureConf{}, } definedEnvs := map[string]struct{}{} err := st.validate(definedEnvs) var environmentNotDefined EnvironmentNotDefinedError if !errors.As(err, &environmentNotDefined) { t.Errorf("Should be EnvironmentNotDefinedError") } t.Logf("%v", environmentNotDefined) } func TestScenarioStepValid_EnvVariableInURL(t *testing.T) { url := "https://test.com/{{ARGENTINA}}" st := ScenarioStep{ ID: 22, Name: "", Method: http.MethodGet, Auth: Auth{}, Cert: tls.Certificate{}, CertPool: &x509.CertPool{}, Headers: map[string]string{}, Payload: "", URL: url, Timeout: 0, Sleep: "", Custom: map[string]interface{}{}, EnvsToCapture: []EnvCaptureConf{}, } definedEnvs := map[string]struct{}{} err := st.validate(definedEnvs) var environmentNotDefined EnvironmentNotDefinedError if !errors.As(err, &environmentNotDefined) { t.Errorf("Should be EnvironmentNotDefinedError") } t.Logf("%v", environmentNotDefined) } func TestScenarioStep_InvalidCaptureConfig(t *testing.T) { url := "https://test.com" stEmptyFromField := ScenarioStep{ ID: 22, Name: "", Method: http.MethodGet, URL: url, EnvsToCapture: []EnvCaptureConf{{ Name: "FromHeader", From: "", }}, } stNoHeaderKey := ScenarioStep{ ID: 22, Name: "", Method: http.MethodGet, URL: url, EnvsToCapture: []EnvCaptureConf{{ Name: "FromHeader", From: SourceType(Header), }}, } stNoBodySpecifierKey := ScenarioStep{ ID: 22, Name: "", Method: http.MethodGet, URL: url, EnvsToCapture: []EnvCaptureConf{{ Name: "FromBody", From: SourceType(Body), }}, } definedEnvs := map[string]struct{}{} tests := []struct { name string st ScenarioStep }{ {"NoHeaderKey", stNoHeaderKey}, {"NoBodySpecifierKey", stNoBodySpecifierKey}, {"EmptyFromField", stEmptyFromField}, } for _, test := range tests { tf := func(t *testing.T) { // Arrange err := test.st.validate(definedEnvs) var captureConfigError CaptureConfigError if !errors.As(err, &captureConfigError) { t.Errorf("Should be CaptureConfigError") } } t.Run(test.name, tf) } } func TestScenarioStepValid_OSEnvVariableInPayload(t *testing.T) { url := "https://test.com" st := ScenarioStep{ ID: 22, Name: "", Method: http.MethodGet, Auth: Auth{}, Cert: tls.Certificate{}, CertPool: &x509.CertPool{}, Headers: map[string]string{}, Payload: "{{$SOME_OS_ENV_XX}}", URL: url, Timeout: 0, Sleep: "", Custom: map[string]interface{}{}, EnvsToCapture: []EnvCaptureConf{}, } definedEnvs := map[string]struct{}{} err := st.validate(definedEnvs) var environmentNotDefined EnvironmentNotDefinedError if !errors.As(err, &environmentNotDefined) { t.Errorf("Should be EnvironmentNotDefinedError") } t.Logf("%v", environmentNotDefined) } ================================================ FILE: ddosify_engine/core/util/buffer_pool.go ================================================ package util import ( "bytes" "errors" ) // Factory is a function to create new connections. type BufferFactoryMethod func() *bytes.Buffer type BufferCloseMethod func(*bytes.Buffer) func NewBufferPool(initialCap, maxCap int, factory BufferFactoryMethod, close BufferCloseMethod) (*Pool[*bytes.Buffer], error) { if initialCap < 0 || maxCap <= 0 || initialCap > maxCap { return nil, errors.New("invalid capacity settings") } pool := &Pool[*bytes.Buffer]{ Items: make(chan *bytes.Buffer, maxCap), Factory: factory, Close: close, } // create initial clients, if something goes wrong, // just close the pool error out. for i := 0; i < initialCap; i++ { client := pool.Factory() pool.Items <- client } return pool, nil } ================================================ FILE: ddosify_engine/core/util/helper.go ================================================ /* * * Ddosify - Load testing tool for any web system. * Copyright (C) 2021 Ddosify (https://ddosify.com) * * This program is free software: you can redistribute it and/or modify * it under the terms of the GNU Affero General Public License as published * by the Free Software Foundation, either version 3 of the License, or * (at your option) any later version. * * This program is distributed in the hope that it will be useful, * but WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * GNU Affero General Public License for more details. * * You should have received a copy of the GNU Affero General Public License * along with this program. If not, see . * */ package util import ( "os" "strings" ) // StringInSlice checks if the given string is in the given list of strings func StringInSlice(a string, list []string) bool { for _, b := range list { if b == a { return true } } return false } // IsSystemInTestMode checks if the system is running for tests. func IsSystemInTestMode() bool { for _, arg := range os.Args { if strings.HasPrefix(arg, "-test.") { return true } } return false } ================================================ FILE: ddosify_engine/core/util/pool.go ================================================ package util type Pool[T any] struct { Items chan T Factory func() T Close func(T) AfterPut func(T) } func (p *Pool[T]) Get() T { var item T select { case item = <-p.Items: default: item = p.Factory() } return item } func (p *Pool[T]) Put(item T) error { if p.Items == nil { // pool is closed, close passed client p.Close(item) return nil } // put the resource back into the pool. If the pool is full, this will // block and the default case will be executed. select { case p.Items <- item: if p.AfterPut != nil { p.AfterPut(item) } return nil default: // pool is full, close passed client p.Close(item) return nil } } func (p *Pool[T]) Len() int { return len(p.Items) } func (p *Pool[T]) Done() { close(p.Items) for i := range p.Items { p.Close(i) } } ================================================ FILE: ddosify_engine/go.mod ================================================ module go.ddosify.com/ddosify go 1.18 require ( github.com/antchfx/xmlquery v1.3.13 github.com/asaskevich/govalidator v0.0.0-20210307081110-f21760c49a8d github.com/ddosify/go-faker v0.1.1 github.com/enescakir/emoji v1.0.0 github.com/fatih/color v1.13.0 github.com/google/uuid v1.3.0 github.com/mattn/go-colorable v0.1.12 github.com/shirou/gopsutil/v3 v3.22.12 github.com/tidwall/gjson v1.14.4 golang.org/x/exp v0.0.0-20230108222341-4b8118a2686a golang.org/x/net v0.8.0 ) require ( github.com/antchfx/htmlquery v1.3.0 github.com/antchfx/xpath v1.2.3 // indirect github.com/go-ole/go-ole v1.2.6 // indirect github.com/golang/groupcache v0.0.0-20210331224755-41bb18bfe9da // indirect github.com/jaswdr/faker v1.10.2 // indirect github.com/lufia/plan9stats v0.0.0-20211012122336-39d0f177ccd0 // indirect github.com/mattn/go-isatty v0.0.14 // indirect github.com/power-devops/perfstat v0.0.0-20210106213030-5aafc221ea8c // indirect github.com/tidwall/match v1.1.1 // indirect github.com/tidwall/pretty v1.2.0 // indirect github.com/tklauser/go-sysconf v0.3.11 // indirect github.com/tklauser/numcpus v0.6.0 // indirect github.com/yusufpapurcu/wmi v1.2.2 // indirect golang.org/x/sys v0.6.0 // indirect golang.org/x/text v0.8.0 // indirect ) ================================================ FILE: ddosify_engine/go.sum ================================================ github.com/antchfx/htmlquery v1.3.0 h1:5I5yNFOVI+egyia5F2s/5Do2nFWxJz41Tr3DyfKD25E= github.com/antchfx/htmlquery v1.3.0/go.mod h1:zKPDVTMhfOmcwxheXUsx4rKJy8KEY/PU6eXr/2SebQ8= github.com/antchfx/xmlquery v1.3.13 h1:wqhTv2BN5MzYg9rnPVtZb3IWP8kW6WV/ebAY0FCTI7Y= github.com/antchfx/xmlquery v1.3.13/go.mod h1:3w2RvQvTz+DaT5fSgsELkSJcdNgkmg6vuXDEuhdwsPQ= github.com/antchfx/xpath v1.2.1 h1:qhp4EW6aCOVr5XIkT+l6LJ9ck/JsUH/yyauNgTQkBF8= github.com/antchfx/xpath v1.2.1/go.mod h1:i54GszH55fYfBmoZXapTHN8T8tkcHfRgLyVwwqzXNcs= github.com/antchfx/xpath v1.2.3 h1:CCZWOzv5bAqjVv0offZ2LVgVYFbeldKQVuLNbViZdes= github.com/antchfx/xpath v1.2.3/go.mod h1:i54GszH55fYfBmoZXapTHN8T8tkcHfRgLyVwwqzXNcs= github.com/asaskevich/govalidator v0.0.0-20210307081110-f21760c49a8d h1:Byv0BzEl3/e6D5CLfI0j/7hiIEtvGVFPCZ7Ei2oq8iQ= github.com/asaskevich/govalidator v0.0.0-20210307081110-f21760c49a8d/go.mod h1:WaHUgvxTVq04UNunO+XhnAqY/wQc+bxr74GqbsZ/Jqw= github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= 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/ddosify/go-faker v0.1.1 h1:S18MhU7p237JLTwkOyjfMND1M/vdTLlEbTvv005kdRY= github.com/ddosify/go-faker v0.1.1/go.mod h1:59U3tEeBJY+7zXwZyuGpmfblEVb9yJ3hTPRPE8PC8SE= github.com/enescakir/emoji v1.0.0 h1:W+HsNql8swfCQFtioDGDHCHri8nudlK1n5p2rHCJoog= github.com/enescakir/emoji v1.0.0/go.mod h1:Bt1EKuLnKDTYpLALApstIkAjdDrS/8IAgTkKp+WKFD0= github.com/fatih/color v1.13.0 h1:8LOYc1KYPPmyKMuN8QV2DNRWNbLo6LZ0iLs8+mlH53w= github.com/fatih/color v1.13.0/go.mod h1:kLAiJbzzSOZDVNGyDpeOxJ47H46qBXwg5ILebYFFOfk= github.com/go-ole/go-ole v1.2.6 h1:/Fpf6oFPoeFik9ty7siob0G6Ke8QvQEuVcuChpwXzpY= github.com/go-ole/go-ole v1.2.6/go.mod h1:pprOEPIfldk/42T2oK7lQ4v4JSDwmV0As9GaiUsvbm0= github.com/golang/groupcache v0.0.0-20200121045136-8c9f03a8e57e h1:1r7pUrabqp18hOBcwBwiTsbnFeTZHV9eER/QT5JVZxY= github.com/golang/groupcache v0.0.0-20200121045136-8c9f03a8e57e/go.mod h1:cIg4eruTrX1D+g88fzRXU5OdNfaM+9IcxsU14FzY7Hc= github.com/golang/groupcache v0.0.0-20210331224755-41bb18bfe9da h1:oI5xCqsCo564l8iNU+DwB5epxmsaqB+rhGL0m5jtYqE= github.com/golang/groupcache v0.0.0-20210331224755-41bb18bfe9da/go.mod h1:cIg4eruTrX1D+g88fzRXU5OdNfaM+9IcxsU14FzY7Hc= github.com/google/go-cmp v0.5.6/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE= github.com/google/go-cmp v0.5.9 h1:O2Tfq5qg4qc4AmwVlvv0oLiVAGB7enBSJ2x2DqQFi38= github.com/google/go-cmp v0.5.9/go.mod h1:17dUlkBOakJ0+DkrSSNjCkIjxS6bF9zb3elmeNGIjoY= github.com/google/uuid v1.3.0 h1:t6JiXgmwXMjEs8VusXIJk2BXHsn+wx8BZdTaoZ5fu7I= github.com/google/uuid v1.3.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo= github.com/jaswdr/faker v1.10.2 h1:GK03wuDqa8V6BE+2VRr3DJ/G4T0iUDCzVoBCj5TM4b8= github.com/jaswdr/faker v1.10.2/go.mod h1:x7ZlyB1AZqwqKZgyQlnqEG8FDptmHlncA5u2zY/yi6w= github.com/lufia/plan9stats v0.0.0-20211012122336-39d0f177ccd0 h1:6E+4a0GO5zZEnZ81pIr0yLvtUWk2if982qA3F3QD6H4= github.com/lufia/plan9stats v0.0.0-20211012122336-39d0f177ccd0/go.mod h1:zJYVVT2jmtg6P3p1VtQj7WsuWi/y4VnjVBn7F8KPB3I= github.com/mattn/go-colorable v0.1.9/go.mod h1:u6P/XSegPjTcexA+o6vUJrdnUu04hMope9wVRipJSqc= github.com/mattn/go-colorable v0.1.12 h1:jF+Du6AlPIjs2BiUiQlKOX0rt3SujHxPnksPKZbaA40= github.com/mattn/go-colorable v0.1.12/go.mod h1:u5H1YNBxpqRaxsYJYSkiCWKzEfiAb1Gb520KVy5xxl4= github.com/mattn/go-isatty v0.0.12/go.mod h1:cbi8OIDigv2wuxKPP5vlRcQ1OAZbq2CE4Kysco4FUpU= github.com/mattn/go-isatty v0.0.14 h1:yVuAays6BHfxijgZPzw+3Zlu5yQgKGP2/hcQbHb7S9Y= github.com/mattn/go-isatty v0.0.14/go.mod h1:7GGIvUiUoEMVVmxf/4nioHXj79iQHKdU27kJ6hsGG94= 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/power-devops/perfstat v0.0.0-20210106213030-5aafc221ea8c h1:ncq/mPwQF4JjgDlrVEn3C11VoGHZN7m8qihwgMEtzYw= github.com/power-devops/perfstat v0.0.0-20210106213030-5aafc221ea8c/go.mod h1:OmDBASR4679mdNQnz2pUhc2G8CO2JrUAVFDRBDP/hJE= github.com/shirou/gopsutil/v3 v3.22.12 h1:oG0ns6poeUSxf78JtOsfygNWuEHYYz8hnnNg7P04TJs= github.com/shirou/gopsutil/v3 v3.22.12/go.mod h1:Xd7P1kwZcp5VW52+9XsirIKd/BROzbb2wdX3Kqlz9uI= github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME= github.com/stretchr/objx v0.4.0/go.mod h1:YvHI0jy2hoMjB+UWwv71VJQ9isScKT/TqJzVSSt89Yw= github.com/stretchr/objx v0.5.0/go.mod h1:Yh+to48EsGEfYuaHDzXPcE3xhTkx73EhmCGUpEOglKo= github.com/stretchr/testify v1.7.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg= github.com/stretchr/testify v1.8.0/go.mod h1:yNjHg4UonilssWZ8iaSj1OCr/vHnekPRkoO+kdMU+MU= github.com/stretchr/testify v1.8.1 h1:w7B6lhMri9wdJUVmEZPGGhZzrYTPvgJArz7wNPgYKsk= github.com/stretchr/testify v1.8.1/go.mod h1:w2LPCIKwWwSfY2zedu0+kehJoqGctiVI29o6fzry7u4= github.com/tidwall/gjson v1.14.4 h1:uo0p8EbA09J7RQaflQ1aBRffTR7xedD2bcIVSYxLnkM= github.com/tidwall/gjson v1.14.4/go.mod h1:/wbyibRr2FHMks5tjHJ5F8dMZh3AcwJEMf5vlfC0lxk= github.com/tidwall/match v1.1.1 h1:+Ho715JplO36QYgwN9PGYNhgZvoUSc9X2c80KVTi+GA= github.com/tidwall/match v1.1.1/go.mod h1:eRSPERbgtNPcGhD8UCthc6PmLEQXEWd3PRB5JTxsfmM= github.com/tidwall/pretty v1.2.0 h1:RWIZEg2iJ8/g6fDDYzMpobmaoGh5OLl4AXtGUGPcqCs= github.com/tidwall/pretty v1.2.0/go.mod h1:ITEVvHYasfjBbM0u2Pg8T2nJnzm8xPwvNhhsoaGGjNU= github.com/tklauser/go-sysconf v0.3.11 h1:89WgdJhk5SNwJfu+GKyYveZ4IaJ7xAkecBo+KdJV0CM= github.com/tklauser/go-sysconf v0.3.11/go.mod h1:GqXfhXY3kiPa0nAXPDIQIWzJbMCB7AmcWpGR8lSZfqI= github.com/tklauser/numcpus v0.6.0 h1:kebhY2Qt+3U6RNK7UqpYNA+tJ23IBEGKkB7JQBfDYms= github.com/tklauser/numcpus v0.6.0/go.mod h1:FEZLMke0lhOUG6w2JadTzp0a+Nl8PF/GFkQ5UVIcaL4= github.com/yuin/goldmark v1.4.13/go.mod h1:6yULJ656Px+3vBD8DxQVa3kxgyrAnzto9xy5taEt/CY= github.com/yusufpapurcu/wmi v1.2.2 h1:KBNDSne4vP5mbSWnJbO+51IMOXJB67QiYCSBrubbPRg= github.com/yusufpapurcu/wmi v1.2.2/go.mod h1:SBZ9tNy3G9/m5Oi98Zks0QjeHVDvuK0qfxQmPyzfmi0= golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w= golang.org/x/crypto v0.0.0-20210921155107-089bfa567519/go.mod h1:GvvjBRRGRdwPK5ydBHafDWAxML/pGHZbMvKqRZ5+Abc= golang.org/x/exp v0.0.0-20230108222341-4b8118a2686a h1:tlXy25amD5A7gOfbXdqCGN5k8ESEed/Ee1E5RcrYnqU= golang.org/x/exp v0.0.0-20230108222341-4b8118a2686a/go.mod h1:CxIveKay+FTh1D0yPZemJVgC/95VzuuOLq5Qi4xnoYc= golang.org/x/mod v0.6.0-dev.0.20220419223038-86c51ed26bb4/go.mod h1:jJ57K6gSWd91VN4djpZkiMVwK6gcyfeH4XE8wZrZaV4= golang.org/x/net v0.0.0-20190620200207-3b0461eec859/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= golang.org/x/net v0.0.0-20210226172049-e18ecbb05110/go.mod h1:m0MpNAwzfU5UDzcl9v0D8zg8gWTRqZa9RBIspLL5mdg= golang.org/x/net v0.0.0-20220127200216-cd36cc0744dd/go.mod h1:CfG3xpIq0wQ8r1q4Su4UZFWDARRcnwPjda9FqA0JpMk= golang.org/x/net v0.0.0-20220722155237-a158d28d115b/go.mod h1:XRhObCWvk6IyKnWLug+ECip1KBveYUHfp+8e9klMJ9c= golang.org/x/net v0.5.0/go.mod h1:DivGGAXEgPSlEBzxGzZI+ZLohi+xUj054jfeKui00ws= golang.org/x/net v0.8.0 h1:Zrh2ngAOFYneWTAIAPethzeaQLuHwhuBkuV6ZiRnUaQ= golang.org/x/net v0.8.0/go.mod h1:QVkue5JL9kW//ek3r6jTKnTFis1tRmNAW2P1shuFdJc= golang.org/x/sync v0.0.0-20190423024810-112230192c58/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= golang.org/x/sync v0.0.0-20220722155255-886fb9371eb4/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= golang.org/x/sys v0.0.0-20190916202348-b4ddaad3f8a3/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20200116001909-b77594299b42/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20200223170610-d5e6a3e2c0ae/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20201119102817-f84b799fce68/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20201204225414-ed752295db88/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20210615035016-665e8c7367d1/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.0.0-20210630005230-0f9fa26af87c/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.0.0-20210927094055-39ccf1dd6fa6/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.0.0-20211216021012-1d35b9e2eb4e/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.0.0-20220520151302-bc2c85ada10a/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.0.0-20220722155257-8c9f86f7a55f/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.2.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.3.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.4.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.6.0 h1:MVltZSvRTcU2ljQOhs94SXPftV6DCNnZViHeQps87pQ= golang.org/x/sys v0.6.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo= golang.org/x/term v0.0.0-20210927222741-03fcf44c2211/go.mod h1:jbD1KX2456YbFQfuXm/mYQcufACuNUgVhRMnK/tPxf8= golang.org/x/term v0.4.0/go.mod h1:9P2UbLfCdcvo3p/nzKvsmas4TnlujnuoV9hGgYzW1lQ= golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ= golang.org/x/text v0.3.3/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ= golang.org/x/text v0.3.7/go.mod h1:u+2+/6zg+i71rQMx5EYifcz6MCKuco9NR6JIITiCfzQ= golang.org/x/text v0.6.0/go.mod h1:mrYo+phRRbMaCq/xk9113O4dZlRixOauAjOtrjsXDZ8= golang.org/x/text v0.8.0 h1:57P1ETyNKtuIjB4SRd15iJxuhj8Gc416Y78H3qgMh68= golang.org/x/text v0.8.0/go.mod h1:e1OnstbJyHTd6l/uOt8jFFHp6TRDWZR/bV3emEE/zU8= golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ= golang.org/x/tools v0.0.0-20191119224855-298f0cb1881e/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo= golang.org/x/tools v0.1.12/go.mod h1:hNGJHUnrk76NpqgfD5Aqm5Crs+Hm0VOH/i9J2+nxYbc= golang.org/x/xerrors v0.0.0-20190717185122-a985d3407aa7/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= golang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA= gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= ================================================ FILE: ddosify_engine/main.go ================================================ /* * * Ddosify - Load testing tool for any web system. * Copyright (C) 2021 Ddosify (https://ddosify.com) * * This program is free software: you can redistribute it and/or modify * it under the terms of the GNU Affero General Public License as published * by the Free Software Foundation, either version 3 of the License, or * (at your option) any later version. * * This program is distributed in the hope that it will be useful, * but WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * GNU Affero General Public License for more details. * * You should have received a copy of the GNU Affero General Public License * along with this program. If not, see . * */ package main import ( "context" "flag" "fmt" "io/ioutil" "net/url" "os" "os/signal" "regexp" "runtime" "strings" "text/tabwriter" "time" "go.ddosify.com/ddosify/config" "go.ddosify.com/ddosify/core" "go.ddosify.com/ddosify/core/proxy" "go.ddosify.com/ddosify/core/types" ) //TODO: what about -preview flag? Users can see how many requests will be sent per second with the given parameters. const headerRegexp = `^*(.+):\s*(.+)` // We might consider to use Viper: https://github.com/spf13/viper var ( iterCount = flag.Int("n", types.DefaultIterCount, "Total iteration count") duration = flag.Int("d", types.DefaultDuration, "Test duration in seconds") loadType = flag.String("l", types.DefaultLoadType, "Type of the load test [linear, incremental, waved]") method = flag.String("m", types.DefaultMethod, "Request Method Type. For Http(s):[GET, POST, PUT, DELETE, UPDATE, PATCH]") payload = flag.String("b", "", "Payload of the network packet (body)") auth = flag.String("a", "", "Basic authentication, username:password") headers header target = flag.String("t", "", "Target URL") timeout = flag.Int("T", types.DefaultTimeout, "Request timeout in seconds") proxyFlag = flag.String("P", "", "Proxy address as protocol://username:password@host:port. Supported proxies [http(s), socks]") output = flag.String("o", types.DefaultOutputType, "Output destination") configPath = flag.String("config", "", "Json config file path. If a config file is provided, other flag values will be ignored") certPath = flag.String("cert_path", "", "A path to a certificate file (usually called 'cert.pem')") certKeyPath = flag.String("cert_key_path", "", "A path to a certificate key file (usually called 'key.pem')") version = flag.Bool("version", false, "Prints version, git commit, built date (utc), go information and quit") debug = flag.Bool("debug", false, "Iterates the scenario once and prints curl-like verbose result") ) var ( GitVersion = "development" GitCommit = "unknown" BuildDate = time.Now().UTC().Format(time.RFC3339) ) func main() { flag.Var(&headers, "h", "Request Headers. Ex: -h 'Accept: text/html' -h 'Content-Type: application/xml'") flag.Parse() if *version { printVersionAndExit() } start() } func start() { h, err := createHammer() if err != nil { exitWithMsg(err.Error()) } if err := h.Validate(); err != nil { exitWithMsg(err.Error()) } run(h) } func createHammer() (h types.Hammer, err error) { if *configPath != "" { // running with config and debug mode set from cli return createHammerFromConfigFile(*debug) } return createHammerFromFlags() } var createHammerFromConfigFile = func(debug bool) (h types.Hammer, err error) { f, err := os.Open(*configPath) if err != nil { return } defer f.Close() byteValue, err := ioutil.ReadAll(f) if err != nil { return } c, err := config.NewConfigReader(byteValue, config.ConfigTypeJson) if err != nil { return } h, err = c.CreateHammer() if err != nil { return } if isFlagPassed("debug") { h.Debug = debug // debug flag from cli overrides debug in config file } return } var run = func(h types.Hammer) { ctx, cancel := context.WithCancel(context.Background()) es, err := core.InitEngineServices(h) if err != nil { exitWithMsg(err.Error()) } engine, err := core.NewEngine(ctx, h, es) if err != nil { exitWithMsg(err.Error()) } err = engine.Init() if err != nil { exitWithMsg(err.Error()) } c := make(chan os.Signal, 1) signal.Notify(c, os.Interrupt) defer func() { signal.Stop(c) cancel() }() go func() { select { case <-c: cancel() case <-ctx.Done(): } }() engine.Start() if engine.IsTestFailed() { os.Exit(1) } } var createHammerFromFlags = func() (h types.Hammer, err error) { if *target == "" { err = fmt.Errorf("Please provide the target url with -t flag") return } s, err := createScenario() if err != nil { return } p, err := createProxy() if err != nil { return } h = types.Hammer{ IterationCount: *iterCount, LoadType: strings.ToLower(*loadType), TestDuration: *duration, Scenario: s, Proxy: p, ReportDestination: *output, Debug: *debug, SingleMode: true, } return } func createProxy() (p proxy.Proxy, err error) { var proxyURL *url.URL if *proxyFlag != "" { proxyURL, err = url.Parse(*proxyFlag) if err != nil { return } } p = proxy.Proxy{ Strategy: proxy.ProxyTypeSingle, Addr: proxyURL, } return } func createScenario() (s types.Scenario, err error) { // Auth var a types.Auth if *auth != "" { creds := strings.Split(*auth, ":") if len(creds) != 2 { err = fmt.Errorf("auth credentials couldn't be parsed") return } a = types.Auth{ Type: types.AuthHttpBasic, Username: creds[0], Password: creds[1], } } err = types.IsTargetValid(*target) if err != nil { return } h, err := parseHeaders(headers) if err != nil { return } step := types.ScenarioStep{ ID: 1, Method: strings.ToUpper(*method), Auth: a, Headers: h, Payload: *payload, URL: *target, Timeout: *timeout, } // TODO : if whether certPath or certKeyPath doesn't exist and another one exists, we should return an error to user. if *certPath != "" && *certKeyPath != "" { cert, pool, e := types.ParseTLS(*certPath, *certKeyPath) if e != nil { err = e return } step.Cert = cert step.CertPool = pool } s = types.Scenario{Steps: []types.ScenarioStep{step}} return } func versionTemplate() string { b := strings.Builder{} w := tabwriter.NewWriter(&b, 0, 0, 5, ' ', 0) fmt.Fprintf(w, "Version:\t%s\n", GitVersion) fmt.Fprintf(w, "Git commit:\t%s\n", GitCommit) fmt.Fprintf(w, "Built\t%s\n", BuildDate) fmt.Fprintf(w, "Go version:\t%s\n", runtime.Version()) fmt.Fprintf(w, "OS/Arch:\t%s\n", fmt.Sprintf("%s/%s", runtime.GOOS, runtime.GOARCH)) w.Flush() return b.String() } func printVersionAndExit() { fmt.Println(versionTemplate()) os.Exit(0) } func exitWithMsg(msg string) { if msg != "" { msg = "err: " + msg fmt.Fprintln(os.Stderr, msg) } os.Exit(1) } func parseHeaders(headersArr []string) (headersMap map[string]string, err error) { re := regexp.MustCompile(headerRegexp) headersMap = make(map[string]string) for _, h := range headersArr { matches := re.FindStringSubmatch(h) if len(matches) < 1 { err = fmt.Errorf("invalid header: %v", h) return } headersMap[matches[1]] = matches[2] } return } type header []string func (h *header) String() string { return fmt.Sprintf("%s - %d", *h, len(*h)) } func (h *header) Set(value string) error { *h = append(*h, value) return nil } func isFlagPassed(name string) bool { found := false flag.Visit(func(f *flag.Flag) { if f.Name == name { found = true } }) return found } ================================================ FILE: ddosify_engine/main_benchmark_test.go ================================================ //go:build linux || darwin // +build linux darwin /* * * Ddosify - Load testing tool for any web system. * Copyright (C) 2021 Ddosify (https://ddosify.com) * * This program is free software: you can redistribute it and/or modify * it under the terms of the GNU Affero General Public License as published * by the Free Software Foundation, either version 3 of the License, or * (at your option) any later version. * * This program is distributed in the hope that it will be useful, * but WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * GNU Affero General Public License for more details. * * You should have received a copy of the GNU Affero General Public License * along with this program. If not, see . * */ package main import ( "flag" "fmt" "log" "os" "runtime" "runtime/pprof" "runtime/trace" "strconv" "strings" "syscall" "testing" "time" "github.com/shirou/gopsutil/v3/cpu" gopsProc "github.com/shirou/gopsutil/v3/process" "golang.org/x/exp/constraints" ) type TestType string const ( Multipart TestType = "multipart" Correlation TestType = "correlation" Basic TestType = "basic" ) var table = []struct { name string path string cpuTimeThreshold float64 // in percents maxMemThreshold float32 avgMemThreshold float32 testType TestType }{ { name: "config_distinct_user", path: "config/config_testdata/benchmark/config_distinct_user.json", cpuTimeThreshold: 0.350, maxMemThreshold: 0.5, avgMemThreshold: 0.35, testType: Basic, }, { name: "config_repeated_user", path: "config/config_testdata/benchmark/config_repeated_user.json", cpuTimeThreshold: 0.350, maxMemThreshold: 0.5, avgMemThreshold: 0.35, testType: Basic, }, { name: "config_correlation_load_1", path: "config/config_testdata/benchmark/config_correlation_load_1.json", cpuTimeThreshold: 0.350, maxMemThreshold: 0.5, avgMemThreshold: 0.35, testType: Correlation, }, { name: "config_correlation_load_2", path: "config/config_testdata/benchmark/config_correlation_load_2.json", cpuTimeThreshold: 2.5, maxMemThreshold: 0.8, avgMemThreshold: 0.7, testType: Correlation, }, { name: "config_correlation_load_3", path: "config/config_testdata/benchmark/config_correlation_load_3.json", cpuTimeThreshold: 15.5, maxMemThreshold: 5, avgMemThreshold: 4, testType: Correlation, }, { name: "config_correlation_load_4", path: "config/config_testdata/benchmark/config_correlation_load_4.json", cpuTimeThreshold: 25, maxMemThreshold: 7, avgMemThreshold: 5, testType: Correlation, }, { name: "config_correlation_load_5", path: "config/config_testdata/benchmark/config_correlation_load_5.json", cpuTimeThreshold: 60, maxMemThreshold: 15, avgMemThreshold: 10, testType: Correlation, }, //{ // name: "config_multipart_inject_10rps", // path: "config/config_testdata/benchmark/config_multipart_inject_10rps.json", // cpuTimeThreshold: 5, // maxMemThreshold: 2, // avgMemThreshold: 1, // testType: Multipart, //}, //{ // name: "config_multipart_inject_100rps", // path: "config/config_testdata/benchmark/config_multipart_inject_100rps.json", // cpuTimeThreshold: 50, // maxMemThreshold: 3, // avgMemThreshold: 2, // testType: Multipart, //}, //{ // name: "config_multipart_inject_200rps", // path: "config/config_testdata/benchmark/config_multipart_inject_200rps.json", // cpuTimeThreshold: 100, // maxMemThreshold: 5, // avgMemThreshold: 4, // testType: Multipart, //}, //{ // name: "config_multipart_inject_500rps", // path: "config/config_testdata/benchmark/config_multipart_inject_500rps.json", // cpuTimeThreshold: 160, // maxMemThreshold: 7, // avgMemThreshold: 10, // testType: Multipart, //}, { name: "config_multipart_inject_1krps", path: "config/config_testdata/benchmark/config_multipart_inject_1krps.json", cpuTimeThreshold: 200, maxMemThreshold: 10, avgMemThreshold: 15, testType: Multipart, }, //{ // name: "config_multipart_inject_2krps", // path: "config/config_testdata/benchmark/config_multipart_inject_2krps.json", // cpuTimeThreshold: 300, // maxMemThreshold: 15, // avgMemThreshold: 20, // testType: Multipart, //}, } var cpuprofile = flag.String("cpuprof", "", "write cpu profiles") var memprofile = flag.String("memprof", "", "write memory profiles") var keepTrace = flag.String("tracef", "", "write execution traces") var runBenchmarkN = flag.Int("runN", 1, "run benchmarks N times") func BenchmarkEngines(t *testing.B) { index := os.Getenv("index") if index == "" { N := 1 if *runBenchmarkN > 1 { N = *runBenchmarkN } // parent success := true originalN := N for i, _ := range table { // open a new process for each test config N = originalN if table[i].testType != Multipart { N = 1 // if not multipart, run only once } for j := 0; j < N; j++ { // run each test config N times time.Sleep(1 * time.Second) // wait for the previous process to finish // start a child env := fmt.Sprintf("index=%d", i) cPid, err := syscall.ForkExec(os.Args[0], os.Args, &syscall.ProcAttr{Files: []uintptr{0, 1, 2}, Env: []string{env}}) if err != nil { panic(err.Error()) } proc, err := os.FindProcess(cPid) if err != nil { panic(err.Error()) } pState, err := proc.Wait() if err != nil { panic(err.Error()) } if !pState.Success() { success = false } } if !success { t.Fail() } } } else { i, _ := strconv.Atoi(index) conf := table[i] outSuffix := ".out" var err error // child proc var cpuProfFile, memProfFile, traceFile *os.File if *cpuprofile != "" { cpuProfFile, err = os.Create(fmt.Sprintf("%s_cpuprof_%s.out", strings.TrimSuffix(*cpuprofile, outSuffix), conf.name)) if err != nil { log.Fatal(err) } pprof.StartCPUProfile(cpuProfFile) defer cpuProfFile.Close() defer pprof.StopCPUProfile() } if *memprofile != "" { // get memory profile at execution finish memProfFile, err = os.Create(fmt.Sprintf("%s_memprof_%s.out", strings.TrimSuffix(*memprofile, outSuffix), conf.name)) if err != nil { log.Fatal("could not create memory profile: ", err) } defer memProfFile.Close() // error handling omitted for example defer func() { pprof.Lookup("allocs").WriteTo(memProfFile, 0) // if you want to check live heap objects: // runtime.GC() // get up-to-date statistics // pprof.Lookup("heap").WriteTo(memProfFile, 0) }() } if *keepTrace != "" { traceFile, err = os.Create(fmt.Sprintf("%s_trace_%s.out", strings.TrimSuffix(*keepTrace, outSuffix), conf.name)) if err != nil { log.Fatalf("failed to create trace output file: %v", err) } defer func() { if err := traceFile.Close(); err != nil { log.Fatalf("failed to close trace file: %v", err) } }() if err := trace.Start(traceFile); err != nil { log.Fatalf("failed to start trace: %v", err) } defer trace.Stop() } success := t.Run(fmt.Sprintf("config_%s", conf.path), func(t *testing.B) { var memPercents []float32 var cpuStats []*cpu.TimesStat *configPath = conf.path run = tempRun doneChan := make(chan struct{}, 1) go func() { ticker := time.NewTicker(100 * time.Millisecond) pid := os.Getpid() proc, _ := gopsProc.NewProcess(int32(pid)) for { select { case <-ticker.C: cpuStat, _ := proc.Times() cpuStats = append(cpuStats, cpuStat) memPerc, _ := proc.MemoryPercent() memPercents = append(memPercents, memPerc) case <-doneChan: return } } }() start() doneChan <- struct{}{} lastCpuStat := cpuStats[len(cpuStats)-1] cpuTime := lastCpuStat.User + lastCpuStat.System fmt.Printf("cpuTime: %f / %f \n", cpuTime, conf.cpuTimeThreshold) avgMem := sum(memPercents) / float32(len(memPercents)) maxMem := max(memPercents) fmt.Printf("Avg mem: %f / %f \n", avgMem, conf.avgMemThreshold) fmt.Printf("Max mem: %f / %f \n\n", maxMem, conf.maxMemThreshold) if cpuTime > conf.cpuTimeThreshold { t.Errorf("Cpu time %f, higher than cpuTimeThreshold %f", cpuTime, conf.cpuTimeThreshold) } if avgMem > conf.avgMemThreshold { t.Errorf("Avg mem %f, higher than avgMemThreshold %f", avgMem, conf.avgMemThreshold) } if maxMem > conf.maxMemThreshold { t.Errorf("Max mem %f, higher than maxMemThreshold %f", maxMem, conf.maxMemThreshold) } }) if !success { runtime.Goexit() } } } func max[T constraints.Ordered](s []T) T { if len(s) == 0 { var zero T return zero } m := s[0] for _, v := range s { if m < v { m = v } } return m } func sum[T constraints.Ordered](s []T) T { if len(s) == 0 { var zero T return zero } var m T for _, v := range s { m += v } return m } ================================================ FILE: ddosify_engine/main_exit_test.go ================================================ //go:build linux || darwin // +build linux darwin /* * * Ddosify - Load testing tool for any web system. * Copyright (C) 2021 Ddosify (https://ddosify.com) * * This program is free software: you can redistribute it and/or modify * it under the terms of the GNU Affero General Public License as published * by the Free Software Foundation, either version 3 of the License, or * (at your option) any later version. * * This program is distributed in the hope that it will be useful, * but WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * GNU Affero General Public License for more details. * * You should have received a copy of the GNU Affero General Public License * along with this program. If not, see . * */ package main import ( "fmt" "os" "syscall" "testing" "go.ddosify.com/ddosify/core/types" ) func TestExitStatusOnTestFail(t *testing.T) { index := os.Getenv("index") if index == "" { // parent // start a test in child proc, look for its exit status env := fmt.Sprintf("index=%d", 1) cPid, err := syscall.ForkExec(os.Args[0], os.Args, &syscall.ProcAttr{Files: []uintptr{0, 1, 2}, Env: []string{env}}) if err != nil { panic(err.Error()) } proc, err := os.FindProcess(cPid) if err != nil { panic(err.Error()) } // expected child to fail with exit code 1 pState, err := proc.Wait() if err != nil { panic(err.Error()) } if pState.Success() { t.Fail() } } else { // run a failed engine *configPath = "config/config_testdata/config_test_assertion_fail.json" run = tempRun start() run = func(h types.Hammer) {} } } ================================================ FILE: ddosify_engine/main_test.go ================================================ /* * * Ddosify - Load testing tool for any web system. * Copyright (C) 2021 Ddosify (https://ddosify.com) * * This program is free software: you can redistribute it and/or modify * it under the terms of the GNU Affero General Public License as published * by the Free Software Foundation, either version 3 of the License, or * (at your option) any later version. * * This program is distributed in the hope that it will be useful, * but WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * GNU Affero General Public License for more details. * * You should have received a copy of the GNU Affero General Public License * along with this program. If not, see . * */ package main import ( "crypto/tls" "flag" "fmt" "io" "net/url" "os" "os/exec" "reflect" "strings" "testing" "go.ddosify.com/ddosify/core/proxy" "go.ddosify.com/ddosify/core/types" ) var tempRun func(h types.Hammer) func TestMain(m *testing.M) { // Mock run function to prevent engine starting tempRun = run run = func(h types.Hammer) {} os.Exit(m.Run()) } func resetFlags() { *iterCount = types.DefaultIterCount *loadType = types.DefaultLoadType *duration = types.DefaultDuration *method = types.DefaultMethod *payload = "" *auth = "" headers = header{} *target = "" *timeout = types.DefaultTimeout *proxyFlag = "" *output = types.DefaultOutputType *configPath = "" *certPath = "" *certKeyPath = "" *debug = false } func TestDefaultFlagValues(t *testing.T) { oldArgs := os.Args defer func() { os.Args = oldArgs }() os.Args = []string{"cmd", "-t=example.com"} flag.Parse() if *iterCount != types.DefaultIterCount { t.Errorf("TestDefaultFlagValues failed, expected %#v, found %#v", types.DefaultIterCount, *iterCount) } if *loadType != types.DefaultLoadType { t.Errorf("TestDefaultFlagValues failed, expected %#v, found %#v", types.DefaultLoadType, *loadType) } if *duration != types.DefaultDuration { t.Errorf("TestDefaultFlagValues failed, expected %#v, found %#v", types.DefaultDuration, *duration) } if *method != types.DefaultMethod { t.Errorf("TestDefaultFlagValues failed, expected %#v, found %#v", types.DefaultMethod, *method) } if *payload != "" { t.Errorf("TestDefaultFlagValues failed, expected %#v, found %#v", "", *payload) } if *auth != "" { t.Errorf("TestDefaultFlagValues failed, expected %#v, found %#v", "", *auth) } if reflect.DeepEqual(headers, []string{}) { t.Errorf("TestDefaultFlagValues failed, expected %#v, found %#v", []string{}, headers) } if *timeout != types.DefaultTimeout { t.Errorf("TestDefaultFlagValues failed, expected %#v, found %#v", types.DefaultTimeout, *timeout) } if *proxyFlag != "" { t.Errorf("TestDefaultFlagValues failed, expected %#v, found %#v", "", *proxyFlag) } if *output != types.DefaultOutputType { t.Errorf("TestDefaultFlagValues failed, expected %#v, found %#v", types.DefaultOutputType, *output) } if *configPath != "" { t.Errorf("TestDefaultFlagValues failed, expected %#v, found %#v", "", *configPath) } if *certPath != "" { t.Errorf("TestDefaultFlagValues failed, expected %#v, found %#v", "", *certPath) } if *certKeyPath != "" { t.Errorf("TestDefaultFlagValues failed, expected %#v, found %#v", "", *certKeyPath) } } func TestCreateHammer(t *testing.T) { tests := []struct { name string args string fromFlags bool fromFile bool }{ {"Flag", "-t=dummy.com -config=", true, false}, {"File", "-config=dummy.json -t=", false, true}, } for _, test := range tests { tf := func(t *testing.T) { // Arrange resetFlags() oldArgs := os.Args oldFileFunc := createHammerFromConfigFile oldFlagFunc := createHammerFromFlags defer func() { os.Args = oldArgs createHammerFromConfigFile = oldFileFunc createHammerFromFlags = oldFlagFunc }() fromFileCalled := false fromFlagsCalled := false createHammerFromConfigFile = func(debug bool) (h types.Hammer, err error) { fromFileCalled = true return } createHammerFromFlags = func() (h types.Hammer, err error) { fromFlagsCalled = true return } // Act os.Args = []string{"cmd", test.args} flag.Parse() createHammer() // Assert if fromFileCalled != test.fromFile { t.Errorf("createHammerFromConfigFileCalled expected %v found %v", test.fromFile, fromFileCalled) } if fromFlagsCalled != test.fromFlags { t.Errorf("createHammerFromFlagsCalled expected %v found %v", test.fromFlags, fromFlagsCalled) } } t.Run(test.name, tf) } } func TestDebugFlagOverridesConfig(t *testing.T) { tests := []struct { name string args []string }{ {"DebugFlagShouldOverrideConfig", []string{"-config", "config/config_testdata/config_debug_false.json", "-debug"}}, {"UseConfigDebugKeyWhenNoDebugFlagSpecified", []string{"-config", "config/config_testdata/config_debug_mode.json", "-debug", "false"}}, } for _, test := range tests { tf := func(t *testing.T) { // Arrange resetFlags() oldArgs := os.Args defer func() { os.Args = oldArgs }() // Act os.Args = append([]string{"cmd"}, test.args...) flag.Parse() h, err := createHammer() if err != nil { t.Errorf("createHammer return %v", err) } // Assert if h.Debug != *debug { t.Errorf("debug flag did not override config file") } } t.Run(test.name, tf) } resetFlags() } func TestCreateScenario(t *testing.T) { url := "https://test.com" valid := types.Scenario{ Steps: []types.ScenarioStep{ { ID: 1, Method: types.DefaultMethod, URL: url, Timeout: types.DefaultTimeout, Headers: map[string]string{}, }, }, } validWithAuth := types.Scenario{ Steps: []types.ScenarioStep{ { ID: 1, Method: types.DefaultMethod, URL: url, Timeout: types.DefaultTimeout, Headers: map[string]string{}, Auth: types.Auth{ Type: types.AuthHttpBasic, Username: "testuser", Password: "pass", }, }, }, } tests := []struct { name string args []string shouldErr bool expected types.Scenario }{ {"InvalidAuth", []string{"-t=https://test.com", "-a=no_pass_included"}, true, types.Scenario{}}, {"InvalidTarget", []string{"-t=asds.x.x.x"}, true, types.Scenario{}}, {"Valid", []string{"-t=https://test.com"}, false, valid}, {"ValidWithAuth", []string{"-t=https://test.com", "-a=testuser:pass"}, false, validWithAuth}, } for _, test := range tests { tf := func(t *testing.T) { // Arrange resetFlags() oldArgs := os.Args defer func() { os.Args = oldArgs }() os.Args = append([]string{"cmd"}, test.args...) // Act flag.Parse() s, err := createScenario() // Assert if test.shouldErr { if err == nil { t.Errorf("Should be errored") } } else { if err != nil { t.Errorf("Errored: %v", err) } if !reflect.DeepEqual(test.expected, s) { t.Errorf("Expected %#v, Found %#v", test.expected, s) } } } t.Run(test.name, tf) } } func TestCreateScenarioTLS(t *testing.T) { // prepare TLS files cert, certKey := generateCerts() certFile, keyFile, err := createCertPairFiles(cert, certKey) if err != nil { t.Fatalf("Failed to prepare certs %v", err) } defer os.Remove(certFile.Name()) defer os.Remove(keyFile.Name()) certVal, _, err := types.ParseTLS(certFile.Name(), keyFile.Name()) if err != nil { t.Fatalf("Failed to gen certs %v", err) } certPathArg := fmt.Sprintf("--cert_path=%s", certFile.Name()) keyPathArg := fmt.Sprintf("--cert_key_path=%s", keyFile.Name()) tests := []struct { name string args []string shouldErr bool expected tls.Certificate }{ {"MissingKey", []string{"-t=https://test.com", certPathArg}, false, tls.Certificate{}}, {"MissingCert", []string{"-t=https://test.com", keyPathArg}, false, tls.Certificate{}}, {"WithTLS", []string{"-t=https://test.com", certPathArg, keyPathArg}, false, certVal}, } for _, test := range tests { tf := func(t *testing.T) { // Arrange resetFlags() oldArgs := os.Args defer func() { os.Args = oldArgs }() os.Args = append([]string{"cmd"}, test.args...) // Act flag.Parse() s, err := createScenario() // Assert if test.shouldErr { if err == nil { t.Errorf("Should be errored") } } else { if err != nil { t.Errorf("Errored: %v", err) } if !reflect.DeepEqual(test.expected, s.Steps[0].Cert) { t.Errorf("Expected %v, Found %v", test.expected, s) } } } t.Run(test.name, tf) } } func TestCreateProxy(t *testing.T) { addr, _ := url.Parse("http://127.0.0.1:80") withAddr := proxy.Proxy{ Strategy: proxy.ProxyTypeSingle, Addr: addr, } withoutAddr := proxy.Proxy{ Strategy: proxy.ProxyTypeSingle, Addr: nil, } tests := []struct { name string args []string shouldErr bool expected proxy.Proxy }{ {"InvalidProxy", []string{"-t=https://test.com", "-P=127.0.0.1:09"}, true, proxy.Proxy{}}, {"ValidWithAddr", []string{"-t=https://test.com", "-P=http://127.0.0.1:80"}, false, withAddr}, {"ValidWithoutAddr", []string{"-t=https://test.com"}, false, withoutAddr}, } for _, test := range tests { tf := func(t *testing.T) { // Arrange resetFlags() oldArgs := os.Args defer func() { os.Args = oldArgs }() os.Args = append([]string{"cmd"}, test.args...) // Act flag.Parse() p, err := createProxy() // Assert t.Log(test.args) if test.shouldErr { if err == nil { t.Errorf("Should be errored") } } else { if err != nil { t.Errorf("Errored: %v", err) } if test.expected.Strategy != p.Strategy { t.Errorf("Expected Strategy %v, Found %v", test.expected.Strategy, p.Strategy) } if (test.expected.Addr != nil && *test.expected.Addr != *p.Addr) || (test.expected.Addr == nil && p.Addr != nil) { t.Errorf("Expected Addr %v, Found %v", test.expected.Addr, p.Addr) } } } t.Run(test.name, tf) } } func TestParseHeaders(t *testing.T) { validSingleHeader := map[string]string{"header": "value"} validMultiHeader := map[string]string{"header-1": "value-1", "header-2": "value-2"} invalidHeader := header{} invalidHeader.Set("invalid|header?: value-1") tests := []struct { name string args header shouldErr bool expected map[string]string }{ {"ValidSingleHeder", []string{"header: value"}, false, validSingleHeader}, {"ValidMultiHeader", []string{"header-1: value-1", "header-2: value-2"}, false, validMultiHeader}, } for _, test := range tests { tf := func(t *testing.T) { headers := header{} for _, h := range test.args { headers.Set(h) } // Arrange h, err := parseHeaders(headers) // Assert if test.shouldErr { if err == nil { t.Errorf("Should be errored") } } else { if err != nil { t.Errorf("Errored: %v", err) } if !reflect.DeepEqual(test.expected, h) { t.Errorf("Expected %#v, Found %#v", test.expected, h) } } } t.Run(test.name, tf) } } func TestRun(t *testing.T) { // Arrange resetFlags() runCalled := false run = func(h types.Hammer) { runCalled = true } oldArgs := os.Args defer func() { os.Args = oldArgs }() // Act os.Args = []string{"cmd", "-t=test.com"} main() // Assert if !runCalled { t.Errorf("Run should be called") } } func TestTargetEmpty(t *testing.T) { // Below cmd code triggers this block if os.Getenv("TARGET_EMPTY") == "1" { resetFlags() oldArgs := os.Args defer func() { os.Args = oldArgs }() os.Args = []string{"cmd", "asd"} main() return } // Since we reexecute the test here, this test case doesnt' increment the coverage. cmd := exec.Command(os.Args[0], "-test.run=TestTargetEmpty") cmd.Env = append(os.Environ(), "TARGET_EMPTY=1") err := cmd.Run() if e, ok := err.(*exec.ExitError); ok && !e.Success() { return } t.Errorf("TestTargetEmpty should be failed with exit code 1 found: %v", err) } func TestTargetInvalidHammer(t *testing.T) { // Below cmd code triggers this block if os.Getenv("TARGET_EMPTY") == "1" { resetFlags() oldArgs := os.Args defer func() { os.Args = oldArgs }() os.Args = []string{"cmd", "-t=dummy.com -l invalidLoadType"} main() return } // Since we reexecute the test here, this test case doesnt' increment the coverage. cmd := exec.Command(os.Args[0], "-test.run=TestTargetEmpty") cmd.Env = append(os.Environ(), "TARGET_EMPTY=1") err := cmd.Run() if e, ok := err.(*exec.ExitError); ok && !e.Success() { return } t.Errorf("TestTargetEmpty should be failed with exit code 1 found: %v", err) } func TestVersion(t *testing.T) { // Below cmd code triggers this block if os.Getenv("VERSION") == "1" { resetFlags() oldArgs := os.Args defer func() { os.Args = oldArgs }() os.Args = []string{"cmd", "-version"} main() return } // Since we reexecute the test here, this test case doesnt' increment the coverage. cmd := exec.Command(os.Args[0], "-test.run=TestVersion") cmd.Env = append(os.Environ(), "VERSION=1") err := cmd.Run() if err == nil { return } t.Errorf("TestVersion should not be failed") } func Test_versionTemplate(t *testing.T) { GitVersion = "v0.0.2" GitCommit = "akjsghsajghas" BuildDate = "2021-10-03T15:16:52Z" tests := []struct { name string want string }{ {name: "version", want: "Version: v0.0.2\nGit commit: akjsghsajghas\nBuilt 2021-10-03T15:16:52Z\n"}, } for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { if got := versionTemplate(); !strings.Contains(got, tt.want) { t.Errorf("versionTemplate() = %v, want %v", got, tt.want) } }) } } func createCertPairFiles(cert string, certKey string) (*os.File, *os.File, error) { certFile, err := os.CreateTemp("", ".pem") if err != nil { return nil, nil, err } _, err = io.WriteString(certFile, cert) if err != nil { return nil, nil, err } keyFile, err := os.CreateTemp("", ".pem") if err != nil { return nil, nil, err } _, err = io.WriteString(keyFile, certKey) if err != nil { return nil, nil, err } return certFile, keyFile, nil } func generateCerts() (string, string) { cert := `-----BEGIN CERTIFICATE----- MIIDazCCAlOgAwIBAgIUS4UhTks8aRCQ1k9IGn437ZyP3MgwDQYJKoZIhvcNAQEL BQAwRTELMAkGA1UEBhMCQVUxEzARBgNVBAgMClNvbWUtU3RhdGUxITAfBgNVBAoM GEludGVybmV0IFdpZGdpdHMgUHR5IEx0ZDAeFw0yMjEwMDUyMjM5MDVaFw0zMjEw MDIyMjM5MDVaMEUxCzAJBgNVBAYTAkFVMRMwEQYDVQQIDApTb21lLVN0YXRlMSEw HwYDVQQKDBhJbnRlcm5ldCBXaWRnaXRzIFB0eSBMdGQwggEiMA0GCSqGSIb3DQEB AQUAA4IBDwAwggEKAoIBAQDMbZctKXBx8v63TXIhM/OB7S6VfPqpzfHufhs6kAHu jfC2ooCUqzqdg0T8bM1bjahYuAbQA1cWKYBsqfd01Po1ltWmbMf7ZvmSB6VN7kC2 Y670zee91dGDQ2yzmorJuIZAtOBVZesYLg8UHSGzSC/smJOrjYidtlbvzOcX0pv3 RCIUrNMed60EpSch/rzAJLzJmwNSQZ4vJHNlNetSkvTi7cxMWfwpcM/rN1hEmP1X J43hJp/TNRZVnEsvs/yggP/FwUjG74mU3KfnWiv91AkkarNTNquEMJ+f4OFqMcnF p0wqg47JTqcAAT0n1B0VB+z0hGXEFMN+IJXsHETZNG+JAgMBAAGjUzBRMB0GA1Ud DgQWBBSIw+qUKQJjXWti5x/Cnn2GueuX5zAfBgNVHSMEGDAWgBSIw+qUKQJjXWti 5x/Cnn2GueuX5zAPBgNVHRMBAf8EBTADAQH/MA0GCSqGSIb3DQEBCwUAA4IBAQAA DXzf8VXi4s2GScNfHf0BzMjpyrtRZ0Wbp2Vfh7OwVR6xcx+pqXNjlydM/vu2LvOK hh7Jbo+JS+o7O24UJ9lLFkCRsZVF+NFqJf+2rdHCaOiZSdZmtjBU0dFuAGS7+lU3 M8P7WCNOm6NAKbs7VZHVcZPzp81SCPQgQIS19xRf4Irbvsijv4YdyL4Qv7aWcclb MdZX9AH9Fx8tJq4VKvUYsCXAD0kuywMLjh+yj5O/2hMvs5rvaQvm2daQNRDNp884 uTLrNF7W7QaKEL06ZpXJoBqdKsiwn577XTDKvzN0XxQrT+xV9VHO7OXblF+Od3/Y SzBR+QiQKy3x+LkOxhkk -----END CERTIFICATE-----` certKey := `-----BEGIN PRIVATE KEY----- MIIEvQIBADANBgkqhkiG9w0BAQEFAASCBKcwggSjAgEAAoIBAQDMbZctKXBx8v63 TXIhM/OB7S6VfPqpzfHufhs6kAHujfC2ooCUqzqdg0T8bM1bjahYuAbQA1cWKYBs qfd01Po1ltWmbMf7ZvmSB6VN7kC2Y670zee91dGDQ2yzmorJuIZAtOBVZesYLg8U HSGzSC/smJOrjYidtlbvzOcX0pv3RCIUrNMed60EpSch/rzAJLzJmwNSQZ4vJHNl NetSkvTi7cxMWfwpcM/rN1hEmP1XJ43hJp/TNRZVnEsvs/yggP/FwUjG74mU3Kfn Wiv91AkkarNTNquEMJ+f4OFqMcnFp0wqg47JTqcAAT0n1B0VB+z0hGXEFMN+IJXs HETZNG+JAgMBAAECggEAM+U6NHfJmNPD/8qER5OFpJ0Ob1qL06F5Yj7XMLWwF9wm mGaGV7dkKOpTD/Wa6Dv82ZDWAeZnLDQa6vr228zZO9Nvp1EEL3kDsCOKvk7WVLbX ikPfKZznE/iA1tNLmkvioPiJ3oQB+2Bt6YA/tuCDcf+FtU43uTm5tiSBIdYQS+Om xN9OEXihk1svxHXQKa/a3nKPVLvdp3P90hDJ0PcRslXSy1V8az+A94JFEnCvnKsK nF2rItCcXkInL0lYHZKgLHQMXGWkNl8e3PA1GZk3yF6LPNtPI1T5Ek9GwkHNw4JZ BL/xEWLKB1qR2Z4I3UbWGVyi418kANv1eISb+49egQKBgQDraSRWB8nM5O3Zl9kT 8S5K924o1oXrO17eqQlVtQVmtUdoVvIBc6uHQZOmV1eHYpr6c95h8apNLexI22AY SWkq9smpCnxLUsdkplwzie0F4bAzD6MCR8WIJxapUSPlyCA+8st1hquYBchKGQhd 6mMY1gzMDacYV/WhtG4E5d0nMQKBgQDeTr793n00VtpKuquFJe6Stu7Ujf64dL0s 3opLovyI0TmtMz5oCqIezwrjqc0Vy0UksWXaz0AboinDP+5n60cTEIt/6H0kryDc dxfSHEA9BBDoQtxOFi3QGcxXbwu0i9QSoexrKY7FhA2xPji6bCcPycthhIrCpUiZ s5gVkjHn2QKBgQCGklxLMbiSgGvXb46Qb9be1AMNJVT427+n2UmUzR6BUC+53boK Sm1LrJkTBerrYdrmQUZnBxcrd40TORT9zTlpbhppn6zeAjwptVAPxlDQg+uNxOqS ayToaC/0KoYy3OxSD8lvLcT56pRMh3LY/RwZHoPCQiu7Js0r21DpS93YgQKBgAuc c09RMprsOmSS0WiX7ZkOIvVJIVfDCSpxySlgLu56dxe7yHOosoUHbVsswEB2KHtd JKPEFWYcFzBSg4I8AK9XOuIIY5jp6L57Hexke1p0fumSrG0LrYLkBg8/Bo58iywZ 9v414nYgipKKXG4oPfYOJShHwvOdrGgSwEvIIgEpAoGAZz0yC9+x+JaoTnyUIRyI +Aj5a4KhYjFtsZhcn/yCZHDqzJNDz6gAu579ey+J2CVOhjtgB5lowsDrHu32Hqnn SEfyTru/ynQ8obwaRzdDYml+On86YWOw+brpMXkN+KB6bs2okE2N68v0qGPakxjt OLDW6kKz5pI4T8lQJhdqjCU= -----END PRIVATE KEY-----` return cert, certKey } ================================================ FILE: ddosify_engine/scripts/install.sh ================================================ #!/bin/sh uname_arch() { arch=$(uname -m) case $arch in x86_64) arch="amd64" ;; x86) arch="386" ;; i686) arch="386" ;; i386) arch="386" ;; aarch64) arch="arm64" ;; armv*) arch="armv6" ;; armv*) arch="armv6" ;; armv*) arch="armv6" ;; esac if [ "$(uname_os)" == "darwin" ]; then arch="all" fi echo ${arch} } uname_os() { os=$(uname -s | tr '[:upper:]' '[:lower:]') echo "$os" } GITHUB_OWNER="ddosify" GITHUB_REPO="ddosify" TAG="latest" INSTALL_DIR="/usr/local/bin/" OS=$(uname_os) ARCH=$(uname_arch) PLATFORM="${OS}/${ARCH}" GITHUB_RELEASES_PAGE=https://github.com/${GITHUB_OWNER}/${GITHUB_REPO}/releases VERSION=$(curl $GITHUB_RELEASES_PAGE/$TAG -sL -H 'Accept:application/json' | tr -s '\n' ' ' | sed 's/.*"tag_name":"//' | sed 's/".*//' | tr -d v) NAME=${GITHUB_REPO}_${VERSION}_${OS}_${ARCH} TARBALL=${NAME}.tar.gz TARBALL_URL=${GITHUB_RELEASES_PAGE}/download/v${VERSION}/${TARBALL} echo "Downloading latest $GITHUB_REPO binary from $TARBALL_URL" tmpfolder=$(mktemp -d) $(curl $TARBALL_URL -sL -o $tmpfolder/$TARBALL) if [ ! -f $tmpfolder/$TARBALL ]; then echo "Can not download. Exiting..." exit 14 fi cd ${tmpfolder} && tar --no-same-owner -xzf "$tmpfolder/$TARBALL" if [ ! -f $tmpfolder/$GITHUB_REPO ]; then echo "Can not find $GITHUB_REPO. Exiting..." exit 15 fi binary=$tmpfolder/$GITHUB_REPO echo "Installing $GITHUB_REPO to $INSTALL_DIR (sudo access required to write to $INSTALL_DIR)" sudo install "$binary" $INSTALL_DIR echo "Installed $GITHUB_REPO to $INSTALL_DIR" echo "Simple usage: ddosify -t https://testserver.ddosify.com" rm -rf "${tmpdir}" ================================================ FILE: ddosify_engine/scripts/testing/benchstat.sh ================================================ #!/bin/bash set -e LIMIT=15 IS_FAILED=0 time_op=$(grep -A1 'time/op' gobench_branch_result.txt |tail -1 | awk '{NF--;NF--;print $NF}' | tr -d + | tr -d %) echo -e "Max. Delta Time op: $time_op / $LIMIT" | tee benchstat.txt if (( $(echo "$time_op > $LIMIT" | bc -l) )); then IS_FAILED=1 fi alloc_op=$(grep -A1 'alloc/op' gobench_branch_result.txt |tail -1 | awk '{NF--;NF--;print $NF}' | tr -d + | tr -d %) echo -e "Max. Delta Alloc op: $alloc_op / $LIMIT" | tee --append benchstat.txt if (( $(echo "$alloc_op > $LIMIT" | bc -l) )); then IS_FAILED=1 fi allocs_op=$(grep -A1 'allocs/op' gobench_branch_result.txt |tail -1 | awk '{NF--;NF--;print $NF}' | tr -d + | tr -d %) echo -e "Max. Delta Allocs op: $allocs_op / $LIMIT" | tee --append benchstat.txt if (( $(echo "$allocs_op > $LIMIT" | bc -l) )); then IS_FAILED=1 fi github_comment=`jq -Rs '.' benchstat.txt` curl -s -H "Authorization: token $1" \ -X POST -d "{\"body\": $github_comment" \ "https://api.github.com/repos/ddosify/ddosify/issues/$2/comments" if [ $IS_FAILED -eq 1 ]; then exit 1 fi ================================================ FILE: selfhosted/README.md ================================================
Anteon logo dark
Anteon logo light

Anteon Self Hosted: Effortless Kubernetes Monitoring and Performance Testing

Anteon Kubernetes Monitoring Service Map Anteon detects high latency service calls on your K8s cluster. So you can easily find the root service causing the problem.

## 📚 Documentation - [🐝 Installing Anteon Self-Hosted](https://getanteon.com/docs/self-hosted/installation/) - [⚙ Installing eBPF Agent (Alaz)](https://getanteon.com/docs/self-hosted/install-ebpf-agent-alaz-on-self-hosted/) - [📰 Upgrading to Self-Hosted Enterprise](https://getanteon.com/docs/self-hosted/upgrading-to-self-hosted-enterprise/) - [📧 Slack Integration](https://getanteon.com/docs/self-hosted/self-hosted-slack-integration/) See the [documentation](https://getanteon.com/docs/self-hosted/) for other guides such as [disabling telemetry](https://getanteon.com/docs/self-hosted/disabling-telemetry-data/). ## 📝 License Anteon Self Hosted is licensed under the [AGPLv3](../LICENSE) ================================================ FILE: selfhosted/VERSION ================================================ 2.6.4 ================================================ FILE: selfhosted/docker-compose.yml ================================================ version: '3.8' services: nginx: image: nginx:1.25.5-alpine ports: - '8014:80' volumes: - ./nginx/default_reverseproxy.conf:/etc/nginx/conf.d/default.conf depends_on: - frontend - backend restart: always networks: - anteon frontend: image: ddosify/selfhosted_frontend:4.1.5 depends_on: - backend restart: always pull_policy: always networks: - anteon backend: image: ddosify/selfhosted_backend:3.2.9 depends_on: - postgres - influxdb - redis-backend - seaweedfs env_file: - .env networks: - anteon restart: always pull_policy: always command: /workspace/start_scripts/start_app_onprem.sh ports: - '8008:8008' backend-celery-worker: image: ddosify/selfhosted_backend:3.2.9 depends_on: - postgres - influxdb - redis-backend - seaweedfs - backend - rabbitmq env_file: - .env networks: - anteon restart: always pull_policy: always command: /workspace/start_scripts/start_celery_worker.sh backend-celery-beat: image: ddosify/selfhosted_backend:3.2.9 depends_on: - postgres - influxdb - redis-backend - seaweedfs - backend - rabbitmq env_file: - .env networks: - anteon restart: always pull_policy: always command: /workspace/start_scripts/start_celery_beat.sh alaz-backend: image: ddosify/selfhosted_alaz_backend:2.3.11 depends_on: - postgres - influxdb - redis-backend - backend env_file: - .env networks: - anteon restart: always pull_policy: always command: /workspace/start_scripts/start_app_onprem.sh ports: - '8009:8008' alaz-backend-celery-worker-1: image: ddosify/selfhosted_alaz_backend:2.3.11 depends_on: - postgres - influxdb - redis-alaz-backend - alaz-backend - rabbitmq - backend env_file: - .env networks: - anteon restart: always pull_policy: always command: /workspace/start_scripts/start_celery_worker.sh alaz-backend-celery-worker-2: image: ddosify/selfhosted_alaz_backend:2.3.11 depends_on: - postgres - influxdb - redis-alaz-backend - alaz-backend - rabbitmq - backend env_file: - .env networks: - anteon restart: always pull_policy: always command: /workspace/start_scripts/start_celery_worker.sh alaz-backend-celery-beat: image: ddosify/selfhosted_alaz_backend:2.3.11 depends_on: - postgres - influxdb - redis-alaz-backend - alaz-backend - rabbitmq - backend env_file: - .env networks: - anteon restart: always pull_policy: always command: /workspace/start_scripts/start_celery_beat.sh alaz-backend-request-writer: image: ddosify/selfhosted_alaz_backend:2.3.11 depends_on: - postgres - influxdb - redis-alaz-backend - alaz-backend - rabbitmq - backend env_file: - .env networks: - anteon restart: always pull_policy: always command: /workspace/start_scripts/start_request_writer.sh hammermanager: ports: - "9901:8001" image: ddosify/selfhosted_hammermanager:2.0.2 depends_on: - postgres - rabbitmq env_file: - .env networks: - anteon restart: always pull_policy: always command: /workspace/start_scripts/start_app.sh hammermanager-celery-worker: image: ddosify/selfhosted_hammermanager:2.0.2 depends_on: - postgres - rabbitmq - hammermanager env_file: - .env networks: - anteon restart: always pull_policy: always command: /workspace/start_scripts/start_celery_worker.sh hammermanager-celery-beat: image: ddosify/selfhosted_hammermanager:2.0.2 depends_on: - postgres - rabbitmq - hammermanager env_file: - .env networks: - anteon restart: always pull_policy: always command: /workspace/start_scripts/start_celery_beat.sh hammer: image: ddosify/selfhosted_hammer:2.0.0 volumes: - hammer_id:/root/uuid depends_on: - rabbitmq - influxdb - hammermanager - seaweedfs env_file: - .env networks: - anteon restart: always pull_policy: always hammerdebug: image: ddosify/selfhosted_hammer:2.0.0 volumes: - hammerdebug_id:/root/uuid depends_on: - rabbitmq - influxdb - hammermanager env_file: - .env environment: - IS_DEBUG=true networks: - anteon restart: always pull_policy: always postgres: image: "postgres:16.2-alpine" volumes: - postgres_data:/var/lib/postgresql/data - ./init_scripts/postgres:/docker-entrypoint-initdb.d env_file: - .env networks: - anteon restart: always rabbitmq: ports: - "6672:5672" image: "rabbitmq:3.13.1-alpine" networks: - anteon restart: always influxdb: ports: - "9086:8086" image: "influxdb:2.6.1-alpine" volumes: - influxdb_data:/var/lib/influxdb - ./init_scripts/influxdb:/docker-entrypoint-initdb.d environment: - DOCKER_INFLUXDB_INIT_MODE=setup - DOCKER_INFLUXDB_INIT_ORG=ddosify - DOCKER_INFLUXDB_INIT_BUCKET=hammerBucket env_file: - .env networks: - anteon restart: always redis-backend: image: "redis:7.2.4-alpine" volumes: - redis_backend_data:/data networks: - anteon restart: always redis-alaz-backend: image: "redis:7.2.4-alpine" volumes: - redis_alaz_backend_data:/data networks: - anteon restart: always seaweedfs: image: chrislusf/seaweedfs:3.64 ports: - "8333:8333" command: 'server -s3 -dir="/data"' networks: - anteon restart: always volumes: - seaweedfs_data:/data prometheus: image: prom/prometheus:v2.37.9 ports: - "9090:9090" command: --config.file=/prometheus/prometheus.yml --storage.tsdb.path=/prometheus --web.console.libraries=/usr/share/prometheus/console_libraries --web.console.templates=/usr/share/prometheus/consoles --storage.tsdb.retention=10d volumes: - ./init_scripts/prometheus/prometheus.yml:/prometheus/prometheus.yml - prometheus_data:/prometheus networks: - anteon restart: always volumes: postgres_data: influxdb_data: redis_backend_data: redis_alaz_backend_data: seaweedfs_data: hammer_id: hammerdebug_id: prometheus_data: networks: anteon: ================================================ FILE: selfhosted/init_scripts/influxdb/01_influxdb_create_buckets.sh ================================================ #!/bin/bash set -e influx bucket create -n hammerBucketDetailed -o ddosify influx bucket create -n hammerBucketIteration -o ddosify ================================================ FILE: selfhosted/init_scripts/postgres/01_postgres_create_dbs.sql ================================================ CREATE DATABASE backend; CREATE DATABASE alazbackend; CREATE DATABASE hammermanager; ================================================ FILE: selfhosted/init_scripts/prometheus/prometheus.yml ================================================ global: scrape_interval: 10s evaluation_interval: 10s alerting: alertmanagers: - static_configs: - targets: rule_files: scrape_configs: - job_name: "backend" metrics_path: '/metrics/scrape' static_configs: - targets: ["alaz-backend:8008"] basic_auth: username: alaz-backend password: jRTyHAbUHYE37hRBgEz ================================================ FILE: selfhosted/install.sh ================================================ #!/bin/bash set -e echo "⚡ Installing Anteon Self Hosted..." echo "🔍 Checking prerequisites..." # Function to check if a port is available is_port_available() { local port="$1" if ! command -v lsof >/dev/null 2>&1; then echo "❌ lsof not found. Please install lsof and try again." exit 1 fi if lsof -i :"$port" >/dev/null 2>&1; then echo "❌ Port $port is already in use. Free up the current port and try again." exit 1 fi } is_port_available 8014 is_port_available 9901 is_port_available 6672 is_port_available 9086 is_port_available 8333 # Check if Git is installed if ! command -v git >/dev/null 2>&1; then echo "❌ Git not found. Please install Git and try again." exit 1 fi # Check if Docker is installed if ! command -v docker >/dev/null 2>&1; then echo "❌ Docker not found. Please install Docker and try again." exit 1 fi # Check if Docker Compose is installed if ! command -v docker-compose >/dev/null 2>&1; then if ! docker compose version >/dev/null 2>&1; then echo "❌ Docker Compose not found. Please install Docker Compose and try again." exit 1 fi fi # Check if Docker is running if ! docker info >/dev/null 2>&1; then echo "❌ Docker is not running. Please start Docker and try again." exit 1 fi echo "🚀 Starting installation of Anteon Self Hosted..." REPO_DIR="$HOME/.anteon" # Check if repository already exists if [ -d "$REPO_DIR" ]; then echo "🔄 Repository already exists at $REPO_DIR - Attempting to update..." cd "$REPO_DIR" git checkout master cd "$REPO_DIR/selfhosted" git pull 2>&1 || { read -p "⚠️ Error updating repository. Clean and update? [Y/n]: " answer answer=${answer:-Y} if [[ $answer =~ ^[Yy]$ ]]; then git reset --hard >/dev/null 2>&1 git clean -fd >/dev/null 2>&1 git pull >/dev/null 2>&1 fi } else # Clone the repository echo "📦 Cloning repository to $REPO_DIR directory..." git clone https://github.com/getanteon/anteon.git "$REPO_DIR" >/dev/null 2>&1 cd "$REPO_DIR" git checkout master >/dev/null 2>&1 cd "$REPO_DIR/selfhosted" fi # Determine which compose command to use COMPOSE_COMMAND="docker-compose" if command -v docker >/dev/null 2>&1 && docker compose version >/dev/null 2>&1; then COMPOSE_COMMAND="docker compose" fi echo "🚀 Deploying Anteon Self Hosted..." $COMPOSE_COMMAND -f "$REPO_DIR/selfhosted/docker-compose.yml" up -d docker pull busybox:1.34.1 >/dev/null 2>&1 echo "" echo "⏳ Waiting for services to be ready..." docker run --rm --network selfhosted_anteon busybox:1.34.1 /bin/sh -c "until nc -z nginx 80 && nc -z backend 8008 && nc -z hammermanager 8001 && nc -z rabbitmq 5672 && nc -z postgres 5432 && nc -z influxdb 8086 && nc -z seaweedfs 8333; do sleep 5; done" echo "✅ Anteon Self Hosted installation complete!" echo "📁 Installation directory: $REPO_DIR/selfhosted" echo "🔥 To remove Anteon Self Hosted, run: cd $REPO_DIR/selfhosted && $COMPOSE_COMMAND down" echo "" echo "🌐 Open http://localhost:8014 in your browser to access the application." ================================================ FILE: selfhosted/nginx/default_reverseproxy.conf ================================================ upstream frontend { server frontend:3000; } upstream backend { server backend:8008; } upstream alaz-backend { server alaz-backend:8008; } server { listen 80; client_max_body_size 96M; http2_max_field_size 64k; http2_max_header_size 512k; error_log /var/log/nginx/error.log error; access_log off; location / { proxy_pass http://frontend; proxy_set_header Host $host; proxy_set_header X-Real-IP $remote_addr; proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for; proxy_set_header X-Forwarded-Proto $scheme; } location /api/ { proxy_pass http://backend; proxy_set_header Host $host; proxy_set_header X-Real-IP $remote_addr; proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for; proxy_set_header X-Forwarded-Proto $scheme; } location /api-alaz/ { proxy_pass http://alaz-backend; proxy_set_header Host $host; proxy_set_header X-Real-IP $remote_addr; proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for; proxy_set_header X-Forwarded-Proto $scheme; } }