mirror of
https://github.com/aykhans/sarin.git
synced 2026-04-15 04:29:35 +00:00
Compare commits
10 Commits
c299fda79d
...
benchmark/
| Author | SHA1 | Date | |
|---|---|---|---|
| a7b5f73069 | |||
| 304fb160f8 | |||
| 44c35e6681 | |||
| 9215fd8767 | |||
|
|
8879a59159 | ||
| 705f6263fe | |||
| 9c5b998cda | |||
| 026d05f1bf | |||
| 844f139a10 | |||
|
|
d767ac6f37 |
1
.github/FUNDING.yml
vendored
1
.github/FUNDING.yml
vendored
@@ -1,2 +1 @@
|
|||||||
buy_me_a_coffee: aykhan
|
buy_me_a_coffee: aykhan
|
||||||
custom: https://commerce.coinbase.com/checkout/0f33d2fb-54a6-44f5-8783-006ebf70d1a0
|
|
||||||
|
|||||||
4
.github/workflows/lint.yaml
vendored
4
.github/workflows/lint.yaml
vendored
@@ -16,7 +16,7 @@ jobs:
|
|||||||
- uses: actions/checkout@v5
|
- uses: actions/checkout@v5
|
||||||
- uses: actions/setup-go@v6
|
- uses: actions/setup-go@v6
|
||||||
with:
|
with:
|
||||||
go-version: 1.26.0
|
go-version: 1.26.1
|
||||||
- name: go fix
|
- name: go fix
|
||||||
run: |
|
run: |
|
||||||
go fix ./...
|
go fix ./...
|
||||||
@@ -24,4 +24,4 @@ jobs:
|
|||||||
- name: golangci-lint
|
- name: golangci-lint
|
||||||
uses: golangci/golangci-lint-action@v9
|
uses: golangci/golangci-lint-action@v9
|
||||||
with:
|
with:
|
||||||
version: v2.9.0
|
version: v2.11.2
|
||||||
|
|||||||
2
.github/workflows/release.yaml
vendored
2
.github/workflows/release.yaml
vendored
@@ -35,7 +35,7 @@ jobs:
|
|||||||
run: |
|
run: |
|
||||||
echo "VERSION=$(git describe --tags --always)" >> $GITHUB_ENV
|
echo "VERSION=$(git describe --tags --always)" >> $GITHUB_ENV
|
||||||
echo "GIT_COMMIT=$(git rev-parse HEAD)" >> $GITHUB_ENV
|
echo "GIT_COMMIT=$(git rev-parse HEAD)" >> $GITHUB_ENV
|
||||||
echo "GO_VERSION=1.26.0" >> $GITHUB_ENV
|
echo "GO_VERSION=1.26.1" >> $GITHUB_ENV
|
||||||
|
|
||||||
- name: Set up Go
|
- name: Set up Go
|
||||||
if: github.event_name == 'release' || inputs.build_binaries
|
if: github.event_name == 'release' || inputs.build_binaries
|
||||||
|
|||||||
@@ -1,4 +1,4 @@
|
|||||||
ARG GO_VERSION=1.26.0
|
ARG GO_VERSION=1.26.1
|
||||||
|
|
||||||
FROM docker.io/library/golang:${GO_VERSION}-alpine AS builder
|
FROM docker.io/library/golang:${GO_VERSION}-alpine AS builder
|
||||||
|
|
||||||
|
|||||||
@@ -3,7 +3,7 @@ version: "3"
|
|||||||
|
|
||||||
vars:
|
vars:
|
||||||
BIN_DIR: ./bin
|
BIN_DIR: ./bin
|
||||||
GOLANGCI_LINT_VERSION: v2.9.0
|
GOLANGCI_LINT_VERSION: v2.11.2
|
||||||
GOLANGCI: "{{.BIN_DIR}}/golangci-lint-{{.GOLANGCI_LINT_VERSION}}"
|
GOLANGCI: "{{.BIN_DIR}}/golangci-lint-{{.GOLANGCI_LINT_VERSION}}"
|
||||||
|
|
||||||
tasks:
|
tasks:
|
||||||
|
|||||||
42
benchmark/README.md
Normal file
42
benchmark/README.md
Normal file
@@ -0,0 +1,42 @@
|
|||||||
|
# Benchmark
|
||||||
|
|
||||||
|
Compares [sarin](https://github.com/aykhans/sarin), [wrk](https://github.com/wg/wrk), and [bombardier](https://github.com/codesenberg/bombardier) against a minimal C HTTP server using epoll.
|
||||||
|
|
||||||
|
## Requirements
|
||||||
|
|
||||||
|
- `sarin`, `wrk`, `bombardier` in PATH
|
||||||
|
- `gcc`
|
||||||
|
|
||||||
|
## Usage
|
||||||
|
|
||||||
|
```bash
|
||||||
|
./benchmark/run.sh
|
||||||
|
```
|
||||||
|
|
||||||
|
Configuration is at the top of `run.sh`:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
DURATION="30s"
|
||||||
|
CONNECTIONS=(50 100 200)
|
||||||
|
ITERATIONS=3
|
||||||
|
```
|
||||||
|
|
||||||
|
## Structure
|
||||||
|
|
||||||
|
```
|
||||||
|
benchmark/
|
||||||
|
run.sh - benchmark script
|
||||||
|
server/
|
||||||
|
server.c - C epoll HTTP server (returns "ok")
|
||||||
|
results/ - output directory (auto-created)
|
||||||
|
```
|
||||||
|
|
||||||
|
## Output
|
||||||
|
|
||||||
|
Each run produces per-tool files:
|
||||||
|
|
||||||
|
- `*.out` - tool stdout (throughput, latency)
|
||||||
|
- `*.time` - `/usr/bin/time -v` output (peak memory, CPU time)
|
||||||
|
- `*_resources.csv` - sampled CPU/memory during run
|
||||||
|
|
||||||
|
A summary table is printed at the end with requests/sec, total requests, elapsed time, and peak memory.
|
||||||
335
benchmark/run.sh
Executable file
335
benchmark/run.sh
Executable file
@@ -0,0 +1,335 @@
|
|||||||
|
#!/usr/bin/env bash
|
||||||
|
set -euo pipefail
|
||||||
|
|
||||||
|
# ─── Configuration ───────────────────────────────────────────────────────────
|
||||||
|
SERVER_PORT=8080
|
||||||
|
SERVER_URL="http://127.0.0.1:${SERVER_PORT}/"
|
||||||
|
DURATION="30s"
|
||||||
|
CONNECTIONS=(50 100 200)
|
||||||
|
ITERATIONS=3
|
||||||
|
WARMUP_DURATION="5s"
|
||||||
|
RESULTS_DIR="benchmark/results/$(date +%Y%m%d_%H%M%S)"
|
||||||
|
|
||||||
|
# ─── Colors ──────────────────────────────────────────────────────────────────
|
||||||
|
RED='\033[0;31m'
|
||||||
|
GREEN='\033[0;32m'
|
||||||
|
YELLOW='\033[1;33m'
|
||||||
|
CYAN='\033[0;36m'
|
||||||
|
BOLD='\033[1m'
|
||||||
|
NC='\033[0m'
|
||||||
|
|
||||||
|
log() { echo -e "${GREEN}[+]${NC} $*"; }
|
||||||
|
warn() { echo -e "${YELLOW}[!]${NC} $*"; }
|
||||||
|
err() { echo -e "${RED}[✗]${NC} $*" >&2; }
|
||||||
|
header() { echo -e "\n${BOLD}${CYAN}═══ $* ═══${NC}\n"; }
|
||||||
|
|
||||||
|
# ─── Dependency checks ──────────────────────────────────────────────────────
|
||||||
|
check_deps() {
|
||||||
|
local missing=()
|
||||||
|
for cmd in wrk bombardier sarin gcc; do
|
||||||
|
if ! command -v "$cmd" &>/dev/null; then
|
||||||
|
missing+=("$cmd")
|
||||||
|
fi
|
||||||
|
done
|
||||||
|
if [[ ${#missing[@]} -gt 0 ]]; then
|
||||||
|
err "Missing dependencies: ${missing[*]}"
|
||||||
|
echo "Install them before running this benchmark."
|
||||||
|
exit 1
|
||||||
|
fi
|
||||||
|
log "All dependencies found"
|
||||||
|
}
|
||||||
|
|
||||||
|
# ─── Build & manage the C server ────────────────────────────────────────────
|
||||||
|
build_server() {
|
||||||
|
header "Building C HTTP server"
|
||||||
|
gcc -O3 -o benchmark/server/bench-server benchmark/server/server.c
|
||||||
|
log "Server built successfully"
|
||||||
|
}
|
||||||
|
|
||||||
|
start_server() {
|
||||||
|
log "Starting server on port ${SERVER_PORT}..."
|
||||||
|
benchmark/server/bench-server &
|
||||||
|
SERVER_PID=$!
|
||||||
|
sleep 1
|
||||||
|
|
||||||
|
if ! kill -0 "$SERVER_PID" 2>/dev/null; then
|
||||||
|
err "Server failed to start"
|
||||||
|
exit 1
|
||||||
|
fi
|
||||||
|
log "Server running (PID: ${SERVER_PID})"
|
||||||
|
}
|
||||||
|
|
||||||
|
stop_server() {
|
||||||
|
if [[ -n "${SERVER_PID:-}" ]] && kill -0 "$SERVER_PID" 2>/dev/null; then
|
||||||
|
kill "$SERVER_PID" 2>/dev/null || true
|
||||||
|
wait "$SERVER_PID" 2>/dev/null || true
|
||||||
|
log "Server stopped"
|
||||||
|
fi
|
||||||
|
}
|
||||||
|
|
||||||
|
trap stop_server EXIT
|
||||||
|
|
||||||
|
# ─── Resource monitoring ────────────────────────────────────────────────────
|
||||||
|
start_monitor() {
|
||||||
|
local tool_name=$1
|
||||||
|
local conns=$2
|
||||||
|
local iter=$3
|
||||||
|
local monitor_file="${RESULTS_DIR}/${tool_name}_c${conns}_i${iter}_resources.csv"
|
||||||
|
|
||||||
|
echo "timestamp,cpu%,mem_kb" > "$monitor_file"
|
||||||
|
|
||||||
|
(
|
||||||
|
while true; do
|
||||||
|
# Find the PID of the tool by name (exclude monitor itself)
|
||||||
|
local pid
|
||||||
|
pid=$(pgrep -x "$tool_name" 2>/dev/null | head -1) || true
|
||||||
|
if [[ -n "$pid" ]]; then
|
||||||
|
local stats
|
||||||
|
stats=$(ps -p "$pid" -o %cpu=,%mem=,rss= 2>/dev/null) || true
|
||||||
|
if [[ -n "$stats" ]]; then
|
||||||
|
local cpu mem_kb
|
||||||
|
cpu=$(echo "$stats" | awk '{print $1}')
|
||||||
|
mem_kb=$(echo "$stats" | awk '{print $3}')
|
||||||
|
echo "$(date +%s),$cpu,$mem_kb" >> "$monitor_file"
|
||||||
|
fi
|
||||||
|
fi
|
||||||
|
sleep 0.5
|
||||||
|
done
|
||||||
|
) &
|
||||||
|
MONITOR_PID=$!
|
||||||
|
}
|
||||||
|
|
||||||
|
stop_monitor() {
|
||||||
|
if [[ -n "${MONITOR_PID:-}" ]] && kill -0 "$MONITOR_PID" 2>/dev/null; then
|
||||||
|
kill "$MONITOR_PID" 2>/dev/null || true
|
||||||
|
wait "$MONITOR_PID" 2>/dev/null || true
|
||||||
|
fi
|
||||||
|
}
|
||||||
|
|
||||||
|
# ─── Benchmark runners ─────────────────────────────────────────────────────
|
||||||
|
run_wrk() {
|
||||||
|
local conns=$1
|
||||||
|
local dur=$2
|
||||||
|
local out_file=$3
|
||||||
|
local threads=$((conns < 10 ? conns : 10))
|
||||||
|
|
||||||
|
/usr/bin/time -v wrk -t"${threads}" -c"${conns}" -d"${dur}" "${SERVER_URL}" \
|
||||||
|
2>"${out_file}.time" | tee "${out_file}.out"
|
||||||
|
}
|
||||||
|
|
||||||
|
run_bombardier() {
|
||||||
|
local conns=$1
|
||||||
|
local dur=$2
|
||||||
|
local out_file=$3
|
||||||
|
|
||||||
|
/usr/bin/time -v bombardier -c "${conns}" -d "${dur}" --print result "${SERVER_URL}" \
|
||||||
|
2>"${out_file}.time" | tee "${out_file}.out"
|
||||||
|
}
|
||||||
|
|
||||||
|
run_sarin() {
|
||||||
|
local conns=$1
|
||||||
|
local dur=$2
|
||||||
|
local out_file=$3
|
||||||
|
|
||||||
|
/usr/bin/time -v sarin -U "${SERVER_URL}" -c "${conns}" -d "${dur}" -q \
|
||||||
|
2>"${out_file}.time" | tee "${out_file}.out"
|
||||||
|
}
|
||||||
|
|
||||||
|
# ─── Warmup ──────────────────────────────────────────────────────────────────
|
||||||
|
warmup() {
|
||||||
|
header "Warming up server"
|
||||||
|
wrk -t4 -c50 -d"${WARMUP_DURATION}" "${SERVER_URL}" > /dev/null 2>&1
|
||||||
|
log "Warmup complete"
|
||||||
|
sleep 2
|
||||||
|
}
|
||||||
|
|
||||||
|
# ─── Extract peak memory from /usr/bin/time -v output ────────────────────────
|
||||||
|
extract_peak_mem() {
|
||||||
|
local time_file=$1
|
||||||
|
grep "Maximum resident set size" "$time_file" 2>/dev/null | awk '{print $NF}' || echo "N/A"
|
||||||
|
}
|
||||||
|
|
||||||
|
# ─── Extract total requests from tool output ─────────────────────────────────
|
||||||
|
extract_requests() {
|
||||||
|
local tool=$1
|
||||||
|
local out_file=$2
|
||||||
|
|
||||||
|
case "$tool" in
|
||||||
|
wrk)
|
||||||
|
# wrk: "312513 requests in 2.10s, ..."
|
||||||
|
grep "requests in" "$out_file" 2>/dev/null | awk '{print $1}' || echo "N/A"
|
||||||
|
;;
|
||||||
|
bombardier)
|
||||||
|
# bombardier: "1xx - 0, 2xx - 100000, 3xx - 0, 4xx - 0, 5xx - 0"
|
||||||
|
# Sum all HTTP code counts
|
||||||
|
grep -E "^\s+1xx" "$out_file" 2>/dev/null | \
|
||||||
|
awk -F'[,-]' '{sum=0; for(i=1;i<=NF;i++){gsub(/[^0-9]/,"",$i); if($i+0>0) sum+=$i} print sum}' || echo "N/A"
|
||||||
|
;;
|
||||||
|
sarin)
|
||||||
|
# sarin table: "│ Total │ 1556177 │ ..."
|
||||||
|
grep -i "total" "$out_file" 2>/dev/null | awk -F'│' '{gsub(/[[:space:]]/, "", $3); print $3}' || echo "N/A"
|
||||||
|
;;
|
||||||
|
esac
|
||||||
|
}
|
||||||
|
|
||||||
|
extract_elapsed() {
|
||||||
|
local time_file=$1
|
||||||
|
grep "wall clock" "$time_file" 2>/dev/null | awk '{print $NF}' || echo "N/A"
|
||||||
|
}
|
||||||
|
|
||||||
|
extract_rps() {
|
||||||
|
local tool=$1
|
||||||
|
local out_file=$2
|
||||||
|
|
||||||
|
case "$tool" in
|
||||||
|
wrk)
|
||||||
|
# wrk: "Requests/sec: 12345.67"
|
||||||
|
grep "Requests/sec" "$out_file" 2>/dev/null | awk '{print $2}' || echo "N/A"
|
||||||
|
;;
|
||||||
|
bombardier)
|
||||||
|
# bombardier: "Reqs/sec 12345.67 ..."
|
||||||
|
grep -i "reqs/sec" "$out_file" 2>/dev/null | awk '{print $2}' || echo "N/A"
|
||||||
|
;;
|
||||||
|
sarin)
|
||||||
|
# sarin doesn't output rps - calculate from total requests and duration
|
||||||
|
local total
|
||||||
|
total=$(extract_requests "sarin" "$out_file")
|
||||||
|
if [[ "$total" != "N/A" && -n "$total" ]]; then
|
||||||
|
local dur_secs
|
||||||
|
dur_secs=$(echo "$DURATION" | sed 's/s$//')
|
||||||
|
awk "BEGIN {printf \"%.2f\", $total / $dur_secs}"
|
||||||
|
else
|
||||||
|
echo "N/A"
|
||||||
|
fi
|
||||||
|
;;
|
||||||
|
esac
|
||||||
|
}
|
||||||
|
|
||||||
|
# ─── Print comparison table ──────────────────────────────────────────────────
|
||||||
|
print_table() {
|
||||||
|
local title=$1
|
||||||
|
local extract_fn=$2
|
||||||
|
shift 2
|
||||||
|
local columns=("$@")
|
||||||
|
|
||||||
|
echo -e "${BOLD}${title}:${NC}"
|
||||||
|
printf "%-12s" ""
|
||||||
|
for col in "${columns[@]}"; do
|
||||||
|
printf "%-18s" "$col"
|
||||||
|
done
|
||||||
|
echo ""
|
||||||
|
|
||||||
|
local tools=("wrk" "bombardier" "sarin")
|
||||||
|
for tool in "${tools[@]}"; do
|
||||||
|
printf "%-12s" "$tool"
|
||||||
|
for col in "${columns[@]}"; do
|
||||||
|
local val
|
||||||
|
val=$($extract_fn "$tool" "$col")
|
||||||
|
printf "%-18s" "${val}"
|
||||||
|
done
|
||||||
|
echo ""
|
||||||
|
done
|
||||||
|
echo ""
|
||||||
|
}
|
||||||
|
|
||||||
|
# ─── Main ────────────────────────────────────────────────────────────────────
|
||||||
|
main() {
|
||||||
|
header "HTTP Load Testing Tool Benchmark"
|
||||||
|
echo "Tools: wrk, bombardier, sarin"
|
||||||
|
echo "Duration: ${DURATION} per run"
|
||||||
|
echo "Connections: ${CONNECTIONS[*]}"
|
||||||
|
echo "Iterations: ${ITERATIONS} per configuration"
|
||||||
|
echo ""
|
||||||
|
|
||||||
|
check_deps
|
||||||
|
|
||||||
|
mkdir -p "${RESULTS_DIR}"
|
||||||
|
log "Results will be saved to ${RESULTS_DIR}/"
|
||||||
|
|
||||||
|
build_server
|
||||||
|
start_server
|
||||||
|
warmup
|
||||||
|
|
||||||
|
local tools=("wrk" "bombardier" "sarin")
|
||||||
|
|
||||||
|
for conns in "${CONNECTIONS[@]}"; do
|
||||||
|
header "Testing with ${conns} connections"
|
||||||
|
|
||||||
|
for tool in "${tools[@]}"; do
|
||||||
|
echo -e "${BOLD}--- ${tool} (${conns} connections) ---${NC}"
|
||||||
|
|
||||||
|
for iter in $(seq 1 "$ITERATIONS"); do
|
||||||
|
local out_file="${RESULTS_DIR}/${tool}_c${conns}_i${iter}"
|
||||||
|
echo -n " Run ${iter}/${ITERATIONS}... "
|
||||||
|
|
||||||
|
start_monitor "$tool" "$conns" "$iter"
|
||||||
|
|
||||||
|
case "$tool" in
|
||||||
|
wrk) run_wrk "$conns" "$DURATION" "$out_file" > /dev/null 2>&1 ;;
|
||||||
|
bombardier) run_bombardier "$conns" "$DURATION" "$out_file" > /dev/null 2>&1 ;;
|
||||||
|
sarin) run_sarin "$conns" "$DURATION" "$out_file" > /dev/null 2>&1 ;;
|
||||||
|
esac
|
||||||
|
|
||||||
|
stop_monitor
|
||||||
|
|
||||||
|
local peak_mem rps elapsed
|
||||||
|
peak_mem=$(extract_peak_mem "${out_file}.time")
|
||||||
|
rps=$(extract_rps "$tool" "${out_file}.out")
|
||||||
|
elapsed=$(extract_elapsed "${out_file}.time")
|
||||||
|
echo -e "done (elapsed: ${elapsed}, rps: ${rps}, peak mem: ${peak_mem} KB)"
|
||||||
|
|
||||||
|
sleep 2
|
||||||
|
done
|
||||||
|
echo ""
|
||||||
|
done
|
||||||
|
done
|
||||||
|
|
||||||
|
# ─── Summary ─────────────────────────────────────────────────────────
|
||||||
|
header "Summary"
|
||||||
|
echo "Raw results saved to: ${RESULTS_DIR}/"
|
||||||
|
echo ""
|
||||||
|
echo "Files per run:"
|
||||||
|
echo " *.out - tool stdout (throughput, latency stats)"
|
||||||
|
echo " *.time - /usr/bin/time output (peak memory, CPU time)"
|
||||||
|
echo " *_resources.csv - sampled CPU/memory during run"
|
||||||
|
echo ""
|
||||||
|
|
||||||
|
local columns=()
|
||||||
|
for conns in "${CONNECTIONS[@]}"; do
|
||||||
|
columns+=("c=${conns}")
|
||||||
|
done
|
||||||
|
|
||||||
|
_get_rps() {
|
||||||
|
local c=${2#c=}
|
||||||
|
extract_rps "$1" "${RESULTS_DIR}/${1}_c${c}_i${ITERATIONS}.out"
|
||||||
|
}
|
||||||
|
|
||||||
|
_get_total() {
|
||||||
|
local c=${2#c=}
|
||||||
|
extract_requests "$1" "${RESULTS_DIR}/${1}_c${c}_i${ITERATIONS}.out"
|
||||||
|
}
|
||||||
|
|
||||||
|
_get_mem() {
|
||||||
|
local c=${2#c=}
|
||||||
|
extract_peak_mem "${RESULTS_DIR}/${1}_c${c}_i${ITERATIONS}.time"
|
||||||
|
}
|
||||||
|
|
||||||
|
_get_elapsed() {
|
||||||
|
local c=${2#c=}
|
||||||
|
extract_elapsed "${RESULTS_DIR}/${1}_c${c}_i${ITERATIONS}.time"
|
||||||
|
}
|
||||||
|
|
||||||
|
print_table "Requests/sec" _get_rps "${columns[@]}"
|
||||||
|
print_table "Total Requests" _get_total "${columns[@]}"
|
||||||
|
print_table "Elapsed Time" _get_elapsed "${columns[@]}"
|
||||||
|
print_table "Peak Memory (KB)" _get_mem "${columns[@]}"
|
||||||
|
|
||||||
|
log "Benchmark complete!"
|
||||||
|
echo ""
|
||||||
|
echo "To inspect individual results:"
|
||||||
|
echo " cat ${RESULTS_DIR}/wrk_c200_i1.out"
|
||||||
|
echo " cat ${RESULTS_DIR}/sarin_c200_i1.out"
|
||||||
|
echo " cat ${RESULTS_DIR}/bombardier_c200_i1.out"
|
||||||
|
}
|
||||||
|
|
||||||
|
main "$@"
|
||||||
114
benchmark/server/server.c
Normal file
114
benchmark/server/server.c
Normal file
@@ -0,0 +1,114 @@
|
|||||||
|
#include <errno.h>
|
||||||
|
#include <fcntl.h>
|
||||||
|
#include <netinet/in.h>
|
||||||
|
#include <netinet/tcp.h>
|
||||||
|
#include <signal.h>
|
||||||
|
#include <stdio.h>
|
||||||
|
#include <stdlib.h>
|
||||||
|
#include <string.h>
|
||||||
|
#include <sys/epoll.h>
|
||||||
|
#include <sys/socket.h>
|
||||||
|
#include <unistd.h>
|
||||||
|
|
||||||
|
#define PORT 8080
|
||||||
|
#define MAX_EVENTS 1024
|
||||||
|
#define BUF_SIZE 4096
|
||||||
|
|
||||||
|
static const char RESPONSE[] =
|
||||||
|
"HTTP/1.1 200 OK\r\n"
|
||||||
|
"Content-Length: 2\r\n"
|
||||||
|
"Content-Type: text/plain\r\n"
|
||||||
|
"Connection: keep-alive\r\n"
|
||||||
|
"\r\n"
|
||||||
|
"ok";
|
||||||
|
|
||||||
|
static const int RESPONSE_LEN = sizeof(RESPONSE) - 1;
|
||||||
|
|
||||||
|
static void set_nonblocking(int fd) {
|
||||||
|
int flags = fcntl(fd, F_GETFL, 0);
|
||||||
|
fcntl(fd, F_SETFL, flags | O_NONBLOCK);
|
||||||
|
}
|
||||||
|
|
||||||
|
int main(void) {
|
||||||
|
signal(SIGPIPE, SIG_IGN);
|
||||||
|
|
||||||
|
int server_fd = socket(AF_INET, SOCK_STREAM, 0);
|
||||||
|
if (server_fd < 0) {
|
||||||
|
perror("socket");
|
||||||
|
return 1;
|
||||||
|
}
|
||||||
|
|
||||||
|
int opt = 1;
|
||||||
|
setsockopt(server_fd, SOL_SOCKET, SO_REUSEADDR, &opt, sizeof(opt));
|
||||||
|
setsockopt(server_fd, SOL_SOCKET, SO_REUSEPORT, &opt, sizeof(opt));
|
||||||
|
setsockopt(server_fd, IPPROTO_TCP, TCP_NODELAY, &opt, sizeof(opt));
|
||||||
|
|
||||||
|
struct sockaddr_in addr = {
|
||||||
|
.sin_family = AF_INET,
|
||||||
|
.sin_port = htons(PORT),
|
||||||
|
.sin_addr.s_addr = htonl(INADDR_LOOPBACK),
|
||||||
|
};
|
||||||
|
|
||||||
|
if (bind(server_fd, (struct sockaddr *)&addr, sizeof(addr)) < 0) {
|
||||||
|
perror("bind");
|
||||||
|
return 1;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (listen(server_fd, SOMAXCONN) < 0) {
|
||||||
|
perror("listen");
|
||||||
|
return 1;
|
||||||
|
}
|
||||||
|
|
||||||
|
set_nonblocking(server_fd);
|
||||||
|
|
||||||
|
int epoll_fd = epoll_create1(0);
|
||||||
|
struct epoll_event ev = {.events = EPOLLIN, .data.fd = server_fd};
|
||||||
|
epoll_ctl(epoll_fd, EPOLL_CTL_ADD, server_fd, &ev);
|
||||||
|
|
||||||
|
struct epoll_event events[MAX_EVENTS];
|
||||||
|
char buf[BUF_SIZE];
|
||||||
|
|
||||||
|
fprintf(stderr, "Listening on http://127.0.0.1:%d\n", PORT);
|
||||||
|
|
||||||
|
for (;;) {
|
||||||
|
int n = epoll_wait(epoll_fd, events, MAX_EVENTS, -1);
|
||||||
|
for (int i = 0; i < n; i++) {
|
||||||
|
if (events[i].data.fd == server_fd) {
|
||||||
|
/* Accept all pending connections */
|
||||||
|
for (;;) {
|
||||||
|
int client_fd = accept(server_fd, NULL, NULL);
|
||||||
|
if (client_fd < 0) {
|
||||||
|
if (errno == EAGAIN || errno == EWOULDBLOCK)
|
||||||
|
break;
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
set_nonblocking(client_fd);
|
||||||
|
int tcp_opt = 1;
|
||||||
|
setsockopt(client_fd, IPPROTO_TCP, TCP_NODELAY, &tcp_opt, sizeof(tcp_opt));
|
||||||
|
struct epoll_event cev = {.events = EPOLLIN | EPOLLET, .data.fd = client_fd};
|
||||||
|
epoll_ctl(epoll_fd, EPOLL_CTL_ADD, client_fd, &cev);
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
int fd = events[i].data.fd;
|
||||||
|
/* Read all available data and respond to each request */
|
||||||
|
for (;;) {
|
||||||
|
ssize_t nread = read(fd, buf, BUF_SIZE);
|
||||||
|
if (nread <= 0) {
|
||||||
|
if (nread == 0 || (errno != EAGAIN && errno != EWOULDBLOCK)) {
|
||||||
|
close(fd);
|
||||||
|
}
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
if (write(fd, RESPONSE, RESPONSE_LEN) < 0) {
|
||||||
|
close(fd);
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
close(server_fd);
|
||||||
|
close(epoll_fd);
|
||||||
|
return 0;
|
||||||
|
}
|
||||||
@@ -14,7 +14,7 @@ import (
|
|||||||
)
|
)
|
||||||
|
|
||||||
func main() {
|
func main() {
|
||||||
ctx, cancel := context.WithCancel(context.Background())
|
ctx, cancel := context.WithCancel(context.Background()) //nolint:gosec // G118: cancel is called in listenForTermination goroutine
|
||||||
go listenForTermination(func() { cancel() })
|
go listenForTermination(func() { cancel() })
|
||||||
|
|
||||||
combinedConfig := config.ReadAllConfigs()
|
combinedConfig := config.ReadAllConfigs()
|
||||||
|
|||||||
12
go.mod
12
go.mod
@@ -1,9 +1,9 @@
|
|||||||
module go.aykhans.me/sarin
|
module go.aykhans.me/sarin
|
||||||
|
|
||||||
go 1.26.0
|
go 1.26.1
|
||||||
|
|
||||||
require (
|
require (
|
||||||
github.com/brianvoe/gofakeit/v7 v7.14.0
|
github.com/brianvoe/gofakeit/v7 v7.14.1
|
||||||
github.com/charmbracelet/bubbles v1.0.0
|
github.com/charmbracelet/bubbles v1.0.0
|
||||||
github.com/charmbracelet/bubbletea v1.3.10
|
github.com/charmbracelet/bubbletea v1.3.10
|
||||||
github.com/charmbracelet/glamour v0.10.0
|
github.com/charmbracelet/glamour v0.10.0
|
||||||
@@ -15,7 +15,7 @@ require (
|
|||||||
github.com/yuin/gopher-lua v1.1.1
|
github.com/yuin/gopher-lua v1.1.1
|
||||||
go.aykhans.me/utils v1.0.7
|
go.aykhans.me/utils v1.0.7
|
||||||
go.yaml.in/yaml/v4 v4.0.0-rc.3
|
go.yaml.in/yaml/v4 v4.0.0-rc.3
|
||||||
golang.org/x/net v0.51.0
|
golang.org/x/net v0.52.0
|
||||||
)
|
)
|
||||||
|
|
||||||
require (
|
require (
|
||||||
@@ -53,7 +53,7 @@ require (
|
|||||||
github.com/xo/terminfo v0.0.0-20220910002029-abceb7e1c41e // indirect
|
github.com/xo/terminfo v0.0.0-20220910002029-abceb7e1c41e // indirect
|
||||||
github.com/yuin/goldmark v1.7.16 // indirect
|
github.com/yuin/goldmark v1.7.16 // indirect
|
||||||
github.com/yuin/goldmark-emoji v1.0.6 // indirect
|
github.com/yuin/goldmark-emoji v1.0.6 // indirect
|
||||||
golang.org/x/sys v0.41.0 // indirect
|
golang.org/x/sys v0.42.0 // indirect
|
||||||
golang.org/x/term v0.40.0 // indirect
|
golang.org/x/term v0.41.0 // indirect
|
||||||
golang.org/x/text v0.34.0 // indirect
|
golang.org/x/text v0.35.0 // indirect
|
||||||
)
|
)
|
||||||
|
|||||||
20
go.sum
20
go.sum
@@ -14,8 +14,8 @@ github.com/aymanbagabas/go-udiff v0.3.1 h1:LV+qyBQ2pqe0u42ZsUEtPiCaUoqgA9gYRDs3v
|
|||||||
github.com/aymanbagabas/go-udiff v0.3.1/go.mod h1:G0fsKmG+P6ylD0r6N/KgQD/nWzgfnl8ZBcNLgcbrw8E=
|
github.com/aymanbagabas/go-udiff v0.3.1/go.mod h1:G0fsKmG+P6ylD0r6N/KgQD/nWzgfnl8ZBcNLgcbrw8E=
|
||||||
github.com/aymerick/douceur v0.2.0 h1:Mv+mAeH1Q+n9Fr+oyamOlAkUNPWPlA8PPGR0QAaYuPk=
|
github.com/aymerick/douceur v0.2.0 h1:Mv+mAeH1Q+n9Fr+oyamOlAkUNPWPlA8PPGR0QAaYuPk=
|
||||||
github.com/aymerick/douceur v0.2.0/go.mod h1:wlT5vV2O3h55X9m7iVYN0TBM0NH/MmbLnd30/FjWUq4=
|
github.com/aymerick/douceur v0.2.0/go.mod h1:wlT5vV2O3h55X9m7iVYN0TBM0NH/MmbLnd30/FjWUq4=
|
||||||
github.com/brianvoe/gofakeit/v7 v7.14.0 h1:R8tmT/rTDJmD2ngpqBL9rAKydiL7Qr2u3CXPqRt59pk=
|
github.com/brianvoe/gofakeit/v7 v7.14.1 h1:a7fe3fonbj0cW3wgl5VwIKfZtiH9C3cLnwcIXWT7sow=
|
||||||
github.com/brianvoe/gofakeit/v7 v7.14.0/go.mod h1:QXuPeBw164PJCzCUZVmgpgHJ3Llj49jSLVkKPMtxtxA=
|
github.com/brianvoe/gofakeit/v7 v7.14.1/go.mod h1:QXuPeBw164PJCzCUZVmgpgHJ3Llj49jSLVkKPMtxtxA=
|
||||||
github.com/charmbracelet/bubbles v1.0.0 h1:12J8/ak/uCZEMQ6KU7pcfwceyjLlWsDLAxB5fXonfvc=
|
github.com/charmbracelet/bubbles v1.0.0 h1:12J8/ak/uCZEMQ6KU7pcfwceyjLlWsDLAxB5fXonfvc=
|
||||||
github.com/charmbracelet/bubbles v1.0.0/go.mod h1:9d/Zd5GdnauMI5ivUIVisuEm3ave1XwXtD1ckyV6r3E=
|
github.com/charmbracelet/bubbles v1.0.0/go.mod h1:9d/Zd5GdnauMI5ivUIVisuEm3ave1XwXtD1ckyV6r3E=
|
||||||
github.com/charmbracelet/bubbletea v1.3.10 h1:otUDHWMMzQSB0Pkc87rm691KZ3SWa4KUlvF9nRvCICw=
|
github.com/charmbracelet/bubbletea v1.3.10 h1:otUDHWMMzQSB0Pkc87rm691KZ3SWa4KUlvF9nRvCICw=
|
||||||
@@ -111,16 +111,16 @@ go.yaml.in/yaml/v4 v4.0.0-rc.3 h1:3h1fjsh1CTAPjW7q/EMe+C8shx5d8ctzZTrLcs/j8Go=
|
|||||||
go.yaml.in/yaml/v4 v4.0.0-rc.3/go.mod h1:aZqd9kCMsGL7AuUv/m/PvWLdg5sjJsZ4oHDEnfPPfY0=
|
go.yaml.in/yaml/v4 v4.0.0-rc.3/go.mod h1:aZqd9kCMsGL7AuUv/m/PvWLdg5sjJsZ4oHDEnfPPfY0=
|
||||||
golang.org/x/exp v0.0.0-20231006140011-7918f672742d h1:jtJma62tbqLibJ5sFQz8bKtEM8rJBtfilJ2qTU199MI=
|
golang.org/x/exp v0.0.0-20231006140011-7918f672742d h1:jtJma62tbqLibJ5sFQz8bKtEM8rJBtfilJ2qTU199MI=
|
||||||
golang.org/x/exp v0.0.0-20231006140011-7918f672742d/go.mod h1:ldy0pHrwJyGW56pPQzzkH36rKxoZW1tw7ZJpeKx+hdo=
|
golang.org/x/exp v0.0.0-20231006140011-7918f672742d/go.mod h1:ldy0pHrwJyGW56pPQzzkH36rKxoZW1tw7ZJpeKx+hdo=
|
||||||
golang.org/x/net v0.51.0 h1:94R/GTO7mt3/4wIKpcR5gkGmRLOuE/2hNGeWq/GBIFo=
|
golang.org/x/net v0.52.0 h1:He/TN1l0e4mmR3QqHMT2Xab3Aj3L9qjbhRm78/6jrW0=
|
||||||
golang.org/x/net v0.51.0/go.mod h1:aamm+2QF5ogm02fjy5Bb7CQ0WMt1/WVM7FtyaTLlA9Y=
|
golang.org/x/net v0.52.0/go.mod h1:R1MAz7uMZxVMualyPXb+VaqGSa3LIaUqk0eEt3w36Sw=
|
||||||
golang.org/x/sys v0.0.0-20210809222454-d867a43fc93e/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
golang.org/x/sys v0.0.0-20210809222454-d867a43fc93e/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||||
golang.org/x/sys v0.6.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
golang.org/x/sys v0.6.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||||
golang.org/x/sys v0.41.0 h1:Ivj+2Cp/ylzLiEU89QhWblYnOE9zerudt9Ftecq2C6k=
|
golang.org/x/sys v0.42.0 h1:omrd2nAlyT5ESRdCLYdm3+fMfNFE/+Rf4bDIQImRJeo=
|
||||||
golang.org/x/sys v0.41.0/go.mod h1:OgkHotnGiDImocRcuBABYBEXf8A9a87e/uXjp9XT3ks=
|
golang.org/x/sys v0.42.0/go.mod h1:4GL1E5IUh+htKOUEOaiffhrAeqysfVGipDYzABqnCmw=
|
||||||
golang.org/x/term v0.40.0 h1:36e4zGLqU4yhjlmxEaagx2KuYbJq3EwY8K943ZsHcvg=
|
golang.org/x/term v0.41.0 h1:QCgPso/Q3RTJx2Th4bDLqML4W6iJiaXFq2/ftQF13YU=
|
||||||
golang.org/x/term v0.40.0/go.mod h1:w2P8uVp06p2iyKKuvXIm7N/y0UCRt3UfJTfZ7oOpglM=
|
golang.org/x/term v0.41.0/go.mod h1:3pfBgksrReYfZ5lvYM0kSO0LIkAl4Yl2bXOkKP7Ec2A=
|
||||||
golang.org/x/text v0.34.0 h1:oL/Qq0Kdaqxa1KbNeMKwQq0reLCCaFtqu2eNuSeNHbk=
|
golang.org/x/text v0.35.0 h1:JOVx6vVDFokkpaq1AEptVzLTpDe9KGpj5tR4/X+ybL8=
|
||||||
golang.org/x/text v0.34.0/go.mod h1:homfLqTYRFyVYemLBFl5GgL/DWEiH5wcsQ5gSh1yziA=
|
golang.org/x/text v0.35.0/go.mod h1:khi/HExzZJ2pGnjenulevKNX1W67CUy0AsXcNubPGCA=
|
||||||
gopkg.in/yaml.v2 v2.4.0 h1:D8xgwECY7CYvx+Y2n4sBz93Jn9JRvxdiyyo8CTfuKaY=
|
gopkg.in/yaml.v2 v2.4.0 h1:D8xgwECY7CYvx+Y2n4sBz93Jn9JRvxdiyyo8CTfuKaY=
|
||||||
gopkg.in/yaml.v2 v2.4.0/go.mod h1:RDklbk79AGWmwhnvt/jBztapEOGDOx6ZbXqjP6csGnQ=
|
gopkg.in/yaml.v2 v2.4.0/go.mod h1:RDklbk79AGWmwhnvt/jBztapEOGDOx6ZbXqjP6csGnQ=
|
||||||
gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA=
|
gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA=
|
||||||
|
|||||||
@@ -418,7 +418,7 @@ func (config Config) Validate() error {
|
|||||||
validationErrors = append(validationErrors, types.NewFieldValidationError("Duration", "0", errors.New("duration must be greater than 0")))
|
validationErrors = append(validationErrors, types.NewFieldValidationError("Duration", "0", errors.New("duration must be greater than 0")))
|
||||||
}
|
}
|
||||||
|
|
||||||
if *config.Timeout < 1 {
|
if config.Timeout == nil || *config.Timeout < 1 {
|
||||||
validationErrors = append(validationErrors, types.NewFieldValidationError("Timeout", "0", errors.New("timeout must be greater than 0")))
|
validationErrors = append(validationErrors, types.NewFieldValidationError("Timeout", "0", errors.New("timeout must be greater than 0")))
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -157,7 +157,7 @@ func (parser ConfigENVParser) Parse() (*Config, error) {
|
|||||||
types.NewFieldParseError(
|
types.NewFieldParseError(
|
||||||
parser.getFullEnvName("DURATION"),
|
parser.getFullEnvName("DURATION"),
|
||||||
duration,
|
duration,
|
||||||
errors.New("invalid value duration, expected a duration string (e.g., '10s', '1h30m')"),
|
errors.New("invalid value for duration, expected a duration string (e.g., '10s', '1h30m')"),
|
||||||
),
|
),
|
||||||
)
|
)
|
||||||
} else {
|
} else {
|
||||||
@@ -173,7 +173,7 @@ func (parser ConfigENVParser) Parse() (*Config, error) {
|
|||||||
types.NewFieldParseError(
|
types.NewFieldParseError(
|
||||||
parser.getFullEnvName("TIMEOUT"),
|
parser.getFullEnvName("TIMEOUT"),
|
||||||
timeout,
|
timeout,
|
||||||
errors.New("invalid value duration, expected a duration string (e.g., '10s', '1h30m')"),
|
errors.New("invalid value for duration, expected a duration string (e.g., '10s', '1h30m')"),
|
||||||
),
|
),
|
||||||
)
|
)
|
||||||
} else {
|
} else {
|
||||||
|
|||||||
@@ -172,7 +172,6 @@ func fasthttpSocksDialerDualStackTimeout(ctx context.Context, proxyURL *url.URL,
|
|||||||
return nil, types.NewProxyDialError(proxyStr, err)
|
return nil, types.NewProxyDialError(proxyStr, err)
|
||||||
}
|
}
|
||||||
|
|
||||||
// Cap DNS resolution to half the timeout to reserve time for dial
|
|
||||||
dnsCtx, dnsCancel := context.WithTimeout(ctx, timeout)
|
dnsCtx, dnsCancel := context.WithTimeout(ctx, timeout)
|
||||||
ips, err := net.DefaultResolver.LookupIP(dnsCtx, "ip", host)
|
ips, err := net.DefaultResolver.LookupIP(dnsCtx, "ip", host)
|
||||||
dnsCancel()
|
dnsCancel()
|
||||||
@@ -244,7 +243,7 @@ func fasthttpHTTPSDialerDualStackTimeout(proxyURL *url.URL, timeout time.Duratio
|
|||||||
}
|
}
|
||||||
|
|
||||||
// Upgrade to TLS
|
// Upgrade to TLS
|
||||||
tlsConn := tls.Client(conn, &tls.Config{ //nolint:gosec
|
tlsConn := tls.Client(conn, &tls.Config{
|
||||||
ServerName: proxyURL.Hostname(),
|
ServerName: proxyURL.Hostname(),
|
||||||
})
|
})
|
||||||
if err := tlsConn.Handshake(); err != nil {
|
if err := tlsConn.Handshake(); err != nil {
|
||||||
|
|||||||
@@ -8,7 +8,7 @@ import (
|
|||||||
func NewDefaultRandSource() rand.Source {
|
func NewDefaultRandSource() rand.Source {
|
||||||
now := time.Now().UnixNano()
|
now := time.Now().UnixNano()
|
||||||
return rand.NewPCG(
|
return rand.NewPCG(
|
||||||
uint64(now), //nolint:gosec // G115: Safe conversion; UnixNano timestamp used as random seed, bit pattern is intentional
|
uint64(now),
|
||||||
uint64(now>>32), //nolint:gosec // G115: Safe conversion; right-shifted timestamp for seed entropy, overflow is acceptable
|
uint64(now>>32),
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -43,19 +43,34 @@ func NewRequestGenerator(
|
|||||||
randSource := NewDefaultRandSource()
|
randSource := NewDefaultRandSource()
|
||||||
//nolint:gosec // G404: Using non-cryptographic rand for load testing, not security
|
//nolint:gosec // G404: Using non-cryptographic rand for load testing, not security
|
||||||
localRand := rand.New(randSource)
|
localRand := rand.New(randSource)
|
||||||
templateFuncMap := NewDefaultTemplateFuncMap(randSource, fileCache)
|
|
||||||
|
|
||||||
pathGenerator, isPathGeneratorDynamic := createTemplateFunc(requestURL.Path, templateFuncMap)
|
// Funcs() is only called if a value actually contains template syntax.
|
||||||
methodGenerator, isMethodGeneratorDynamic := NewMethodGeneratorFunc(localRand, methods, templateFuncMap)
|
// The root template is shared across all createTemplateFunc calls so Funcs() is called at most once.
|
||||||
paramsGenerator, isParamsGeneratorDynamic := NewParamsGeneratorFunc(localRand, params, templateFuncMap)
|
var templateRoot *template.Template
|
||||||
headersGenerator, isHeadersGeneratorDynamic := NewHeadersGeneratorFunc(localRand, headers, templateFuncMap)
|
lazyTemplateRoot := func() *template.Template {
|
||||||
cookiesGenerator, isCookiesGeneratorDynamic := NewCookiesGeneratorFunc(localRand, cookies, templateFuncMap)
|
if templateRoot == nil {
|
||||||
|
templateRoot = template.New("").Funcs(NewDefaultTemplateFuncMap(randSource, fileCache))
|
||||||
|
}
|
||||||
|
return templateRoot
|
||||||
|
}
|
||||||
|
|
||||||
|
pathGenerator, isPathGeneratorDynamic := createTemplateFunc(requestURL.Path, lazyTemplateRoot)
|
||||||
|
methodGenerator, isMethodGeneratorDynamic := NewMethodGeneratorFunc(localRand, methods, lazyTemplateRoot)
|
||||||
|
paramsGenerator, isParamsGeneratorDynamic := NewParamsGeneratorFunc(localRand, params, lazyTemplateRoot)
|
||||||
|
headersGenerator, isHeadersGeneratorDynamic := NewHeadersGeneratorFunc(localRand, headers, lazyTemplateRoot)
|
||||||
|
cookiesGenerator, isCookiesGeneratorDynamic := NewCookiesGeneratorFunc(localRand, cookies, lazyTemplateRoot)
|
||||||
|
|
||||||
bodyTemplateFuncMapData := &BodyTemplateFuncMapData{}
|
bodyTemplateFuncMapData := &BodyTemplateFuncMapData{}
|
||||||
bodyTemplateFuncMap := NewDefaultBodyTemplateFuncMap(randSource, bodyTemplateFuncMapData, fileCache)
|
var bodyTemplateRoot *template.Template
|
||||||
bodyGenerator, isBodyGeneratorDynamic := NewBodyGeneratorFunc(localRand, bodies, bodyTemplateFuncMap)
|
lazyBodyTemplateRoot := func() *template.Template {
|
||||||
|
if bodyTemplateRoot == nil {
|
||||||
|
bodyTemplateRoot = template.New("").Funcs(NewDefaultBodyTemplateFuncMap(randSource, bodyTemplateFuncMapData, fileCache))
|
||||||
|
}
|
||||||
|
return bodyTemplateRoot
|
||||||
|
}
|
||||||
|
bodyGenerator, isBodyGeneratorDynamic := NewBodyGeneratorFunc(localRand, bodies, lazyBodyTemplateRoot)
|
||||||
|
|
||||||
valuesGenerator := NewValuesGeneratorFunc(values, templateFuncMap)
|
valuesGenerator := NewValuesGeneratorFunc(values, lazyTemplateRoot)
|
||||||
|
|
||||||
hasScripts := scriptTransformer != nil && !scriptTransformer.IsEmpty()
|
hasScripts := scriptTransformer != nil && !scriptTransformer.IsEmpty()
|
||||||
|
|
||||||
@@ -91,7 +106,7 @@ func NewRequestGenerator(
|
|||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
|
|
||||||
bodyTemplateFuncMapData.ClearFormDataContenType()
|
bodyTemplateFuncMapData.ClearFormDataContentType()
|
||||||
if err = bodyGenerator(reqData, data); err != nil {
|
if err = bodyGenerator(reqData, data); err != nil {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
@@ -99,8 +114,8 @@ func NewRequestGenerator(
|
|||||||
if err = headersGenerator(reqData, data); err != nil {
|
if err = headersGenerator(reqData, data); err != nil {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
if bodyTemplateFuncMapData.GetFormDataContenType() != "" {
|
if bodyTemplateFuncMapData.GetFormDataContentType() != "" {
|
||||||
reqData.Headers["Content-Type"] = append(reqData.Headers["Content-Type"], bodyTemplateFuncMapData.GetFormDataContenType())
|
reqData.Headers["Content-Type"] = append(reqData.Headers["Content-Type"], bodyTemplateFuncMapData.GetFormDataContentType())
|
||||||
}
|
}
|
||||||
|
|
||||||
if err = paramsGenerator(reqData, data); err != nil {
|
if err = paramsGenerator(reqData, data); err != nil {
|
||||||
@@ -170,8 +185,8 @@ func applyRequestDataToFastHTTP(reqData *script.RequestData, req *fasthttp.Reque
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
func NewMethodGeneratorFunc(localRand *rand.Rand, methods []string, templateFunctions template.FuncMap) (requestDataGenerator, bool) {
|
func NewMethodGeneratorFunc(localRand *rand.Rand, methods []string, lazyRoot func() *template.Template) (requestDataGenerator, bool) {
|
||||||
methodGenerator, isDynamic := buildStringSliceGenerator(localRand, methods, templateFunctions)
|
methodGenerator, isDynamic := buildStringSliceGenerator(localRand, methods, lazyRoot)
|
||||||
|
|
||||||
var (
|
var (
|
||||||
method string
|
method string
|
||||||
@@ -188,8 +203,8 @@ func NewMethodGeneratorFunc(localRand *rand.Rand, methods []string, templateFunc
|
|||||||
}, isDynamic
|
}, isDynamic
|
||||||
}
|
}
|
||||||
|
|
||||||
func NewBodyGeneratorFunc(localRand *rand.Rand, bodies []string, templateFunctions template.FuncMap) (requestDataGenerator, bool) {
|
func NewBodyGeneratorFunc(localRand *rand.Rand, bodies []string, lazyRoot func() *template.Template) (requestDataGenerator, bool) {
|
||||||
bodyGenerator, isDynamic := buildStringSliceGenerator(localRand, bodies, templateFunctions)
|
bodyGenerator, isDynamic := buildStringSliceGenerator(localRand, bodies, lazyRoot)
|
||||||
|
|
||||||
var (
|
var (
|
||||||
body string
|
body string
|
||||||
@@ -206,8 +221,8 @@ func NewBodyGeneratorFunc(localRand *rand.Rand, bodies []string, templateFunctio
|
|||||||
}, isDynamic
|
}, isDynamic
|
||||||
}
|
}
|
||||||
|
|
||||||
func NewParamsGeneratorFunc(localRand *rand.Rand, params types.Params, templateFunctions template.FuncMap) (requestDataGenerator, bool) {
|
func NewParamsGeneratorFunc(localRand *rand.Rand, params types.Params, lazyRoot func() *template.Template) (requestDataGenerator, bool) {
|
||||||
generators, isDynamic := buildKeyValueGenerators(localRand, params, templateFunctions)
|
generators, isDynamic := buildKeyValueGenerators(localRand, params, lazyRoot)
|
||||||
|
|
||||||
var (
|
var (
|
||||||
key, value string
|
key, value string
|
||||||
@@ -231,8 +246,8 @@ func NewParamsGeneratorFunc(localRand *rand.Rand, params types.Params, templateF
|
|||||||
}, isDynamic
|
}, isDynamic
|
||||||
}
|
}
|
||||||
|
|
||||||
func NewHeadersGeneratorFunc(localRand *rand.Rand, headers types.Headers, templateFunctions template.FuncMap) (requestDataGenerator, bool) {
|
func NewHeadersGeneratorFunc(localRand *rand.Rand, headers types.Headers, lazyRoot func() *template.Template) (requestDataGenerator, bool) {
|
||||||
generators, isDynamic := buildKeyValueGenerators(localRand, headers, templateFunctions)
|
generators, isDynamic := buildKeyValueGenerators(localRand, headers, lazyRoot)
|
||||||
|
|
||||||
var (
|
var (
|
||||||
key, value string
|
key, value string
|
||||||
@@ -256,8 +271,8 @@ func NewHeadersGeneratorFunc(localRand *rand.Rand, headers types.Headers, templa
|
|||||||
}, isDynamic
|
}, isDynamic
|
||||||
}
|
}
|
||||||
|
|
||||||
func NewCookiesGeneratorFunc(localRand *rand.Rand, cookies types.Cookies, templateFunctions template.FuncMap) (requestDataGenerator, bool) {
|
func NewCookiesGeneratorFunc(localRand *rand.Rand, cookies types.Cookies, lazyRoot func() *template.Template) (requestDataGenerator, bool) {
|
||||||
generators, isDynamic := buildKeyValueGenerators(localRand, cookies, templateFunctions)
|
generators, isDynamic := buildKeyValueGenerators(localRand, cookies, lazyRoot)
|
||||||
|
|
||||||
var (
|
var (
|
||||||
key, value string
|
key, value string
|
||||||
@@ -281,11 +296,11 @@ func NewCookiesGeneratorFunc(localRand *rand.Rand, cookies types.Cookies, templa
|
|||||||
}, isDynamic
|
}, isDynamic
|
||||||
}
|
}
|
||||||
|
|
||||||
func NewValuesGeneratorFunc(values []string, templateFunctions template.FuncMap) func() (valuesData, error) {
|
func NewValuesGeneratorFunc(values []string, lazyRoot func() *template.Template) func() (valuesData, error) {
|
||||||
generators := make([]func(_ any) (string, error), len(values))
|
generators := make([]func(_ any) (string, error), len(values))
|
||||||
|
|
||||||
for i, v := range values {
|
for i, v := range values {
|
||||||
generators[i], _ = createTemplateFunc(v, templateFunctions)
|
generators[i], _ = createTemplateFunc(v, lazyRoot)
|
||||||
}
|
}
|
||||||
|
|
||||||
var (
|
var (
|
||||||
@@ -313,8 +328,12 @@ func NewValuesGeneratorFunc(values []string, templateFunctions template.FuncMap)
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
func createTemplateFunc(value string, templateFunctions template.FuncMap) (func(data any) (string, error), bool) {
|
func createTemplateFunc(value string, lazyRoot func() *template.Template) (func(data any) (string, error), bool) {
|
||||||
tmpl, err := template.New("").Funcs(templateFunctions).Parse(value)
|
if !strings.Contains(value, "{{") {
|
||||||
|
return func(_ any) (string, error) { return value, nil }, false
|
||||||
|
}
|
||||||
|
|
||||||
|
tmpl, err := lazyRoot().New("").Parse(value)
|
||||||
if err == nil && hasTemplateActions(tmpl) {
|
if err == nil && hasTemplateActions(tmpl) {
|
||||||
var err error
|
var err error
|
||||||
return func(data any) (string, error) {
|
return func(data any) (string, error) {
|
||||||
@@ -340,7 +359,7 @@ type keyValueItem interface {
|
|||||||
func buildKeyValueGenerators[T keyValueItem](
|
func buildKeyValueGenerators[T keyValueItem](
|
||||||
localRand *rand.Rand,
|
localRand *rand.Rand,
|
||||||
items []T,
|
items []T,
|
||||||
templateFunctions template.FuncMap,
|
lazyRoot func() *template.Template,
|
||||||
) ([]keyValueGenerator, bool) {
|
) ([]keyValueGenerator, bool) {
|
||||||
isDynamic := false
|
isDynamic := false
|
||||||
generators := make([]keyValueGenerator, len(items))
|
generators := make([]keyValueGenerator, len(items))
|
||||||
@@ -350,7 +369,7 @@ func buildKeyValueGenerators[T keyValueItem](
|
|||||||
keyValue := types.KeyValue[string, []string](item)
|
keyValue := types.KeyValue[string, []string](item)
|
||||||
|
|
||||||
// Generate key function
|
// Generate key function
|
||||||
keyFunc, keyIsDynamic := createTemplateFunc(keyValue.Key, templateFunctions)
|
keyFunc, keyIsDynamic := createTemplateFunc(keyValue.Key, lazyRoot)
|
||||||
if keyIsDynamic {
|
if keyIsDynamic {
|
||||||
isDynamic = true
|
isDynamic = true
|
||||||
}
|
}
|
||||||
@@ -358,7 +377,7 @@ func buildKeyValueGenerators[T keyValueItem](
|
|||||||
// Generate value functions
|
// Generate value functions
|
||||||
valueFuncs := make([]func(data any) (string, error), len(keyValue.Value))
|
valueFuncs := make([]func(data any) (string, error), len(keyValue.Value))
|
||||||
for j, v := range keyValue.Value {
|
for j, v := range keyValue.Value {
|
||||||
valueFunc, valueIsDynamic := createTemplateFunc(v, templateFunctions)
|
valueFunc, valueIsDynamic := createTemplateFunc(v, lazyRoot)
|
||||||
if valueIsDynamic {
|
if valueIsDynamic {
|
||||||
isDynamic = true
|
isDynamic = true
|
||||||
}
|
}
|
||||||
@@ -381,7 +400,7 @@ func buildKeyValueGenerators[T keyValueItem](
|
|||||||
func buildStringSliceGenerator(
|
func buildStringSliceGenerator(
|
||||||
localRand *rand.Rand,
|
localRand *rand.Rand,
|
||||||
values []string,
|
values []string,
|
||||||
templateFunctions template.FuncMap,
|
lazyRoot func() *template.Template,
|
||||||
) (func() func(data any) (string, error), bool) {
|
) (func() func(data any) (string, error), bool) {
|
||||||
// Return a function that returns an empty string generator if values is empty
|
// Return a function that returns an empty string generator if values is empty
|
||||||
if len(values) == 0 {
|
if len(values) == 0 {
|
||||||
@@ -393,7 +412,7 @@ func buildStringSliceGenerator(
|
|||||||
valueFuncs := make([]func(data any) (string, error), len(values))
|
valueFuncs := make([]func(data any) (string, error), len(values))
|
||||||
|
|
||||||
for i, value := range values {
|
for i, value := range values {
|
||||||
valueFunc, valueIsDynamic := createTemplateFunc(value, templateFunctions)
|
valueFunc, valueIsDynamic := createTemplateFunc(value, lazyRoot)
|
||||||
if valueIsDynamic {
|
if valueIsDynamic {
|
||||||
isDynamic = true
|
isDynamic = true
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -484,13 +484,11 @@ func newHostClients(
|
|||||||
proxiesRaw[i] = url.URL(proxy)
|
proxiesRaw[i] = url.URL(proxy)
|
||||||
}
|
}
|
||||||
|
|
||||||
maxConns := max(fasthttp.DefaultMaxConnsPerHost, workers)
|
|
||||||
maxConns = ((maxConns * 50 / 100) + maxConns)
|
|
||||||
return NewHostClients(
|
return NewHostClients(
|
||||||
ctx,
|
ctx,
|
||||||
timeout,
|
timeout,
|
||||||
proxiesRaw,
|
proxiesRaw,
|
||||||
maxConns,
|
workers,
|
||||||
requestURL,
|
requestURL,
|
||||||
skipCertVerify,
|
skipCertVerify,
|
||||||
)
|
)
|
||||||
|
|||||||
@@ -286,7 +286,7 @@ func NewDefaultTemplateFuncMap(randSource rand.Source, fileCache *FileCache) tem
|
|||||||
"fakeit_AdverbFrequencyDefinite": fakeit.AdverbFrequencyDefinite,
|
"fakeit_AdverbFrequencyDefinite": fakeit.AdverbFrequencyDefinite,
|
||||||
"fakeit_AdverbFrequencyIndefinite": fakeit.AdverbFrequencyIndefinite,
|
"fakeit_AdverbFrequencyIndefinite": fakeit.AdverbFrequencyIndefinite,
|
||||||
|
|
||||||
// Propositions
|
// Prepositions
|
||||||
"fakeit_Preposition": fakeit.Preposition,
|
"fakeit_Preposition": fakeit.Preposition,
|
||||||
"fakeit_PrepositionSimple": fakeit.PrepositionSimple,
|
"fakeit_PrepositionSimple": fakeit.PrepositionSimple,
|
||||||
"fakeit_PrepositionDouble": fakeit.PrepositionDouble,
|
"fakeit_PrepositionDouble": fakeit.PrepositionDouble,
|
||||||
@@ -589,15 +589,15 @@ func NewDefaultTemplateFuncMap(randSource rand.Source, fileCache *FileCache) tem
|
|||||||
}
|
}
|
||||||
|
|
||||||
type BodyTemplateFuncMapData struct {
|
type BodyTemplateFuncMapData struct {
|
||||||
formDataContenType string
|
formDataContentType string
|
||||||
}
|
}
|
||||||
|
|
||||||
func (data BodyTemplateFuncMapData) GetFormDataContenType() string {
|
func (data BodyTemplateFuncMapData) GetFormDataContentType() string {
|
||||||
return data.formDataContenType
|
return data.formDataContentType
|
||||||
}
|
}
|
||||||
|
|
||||||
func (data *BodyTemplateFuncMapData) ClearFormDataContenType() {
|
func (data *BodyTemplateFuncMapData) ClearFormDataContentType() {
|
||||||
data.formDataContenType = ""
|
data.formDataContentType = ""
|
||||||
}
|
}
|
||||||
|
|
||||||
func NewDefaultBodyTemplateFuncMap(
|
func NewDefaultBodyTemplateFuncMap(
|
||||||
@@ -628,7 +628,7 @@ func NewDefaultBodyTemplateFuncMap(
|
|||||||
|
|
||||||
var multipartData bytes.Buffer
|
var multipartData bytes.Buffer
|
||||||
writer := multipart.NewWriter(&multipartData)
|
writer := multipart.NewWriter(&multipartData)
|
||||||
data.formDataContenType = writer.FormDataContentType()
|
data.formDataContentType = writer.FormDataContentType()
|
||||||
|
|
||||||
for i := 0; i < len(pairs); i += 2 {
|
for i := 0; i < len(pairs); i += 2 {
|
||||||
key := pairs[i]
|
key := pairs[i]
|
||||||
|
|||||||
Reference in New Issue
Block a user