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
================================================
eBPF-powered Kubernetes Monitoring and Performance Testing
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.
## 🐝 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 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 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.
## ℹ️ 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
================================================
Ddosify: A high-performance load testing tool
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:

_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:

#### Waved
```bash
ddosify -t https://getanteon.com -l waved
```
Result:

### 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 Self Hosted: Effortless Kubernetes Monitoring and Performance Testing
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;
}
}