mirror of
https://github.com/DeNNiiInc/Website-Stress-Test.git
synced 2026-04-17 12:36:00 +00:00
Optimize for high concurrency: Node.js clustering, Web Workers, and system tuning
This commit is contained in:
6
.gitignore
vendored
6
.gitignore
vendored
@@ -1,3 +1,3 @@
|
|||||||
node_modules/
|
node_modules/
|
||||||
deploy-config.json
|
deploy-config.json
|
||||||
.env
|
.env
|
||||||
|
|||||||
@@ -1,211 +1,211 @@
|
|||||||
# 🚀 Proxmox Deployment Template (TurnKey Node.js)
|
# 🚀 Proxmox Deployment Template (TurnKey Node.js)
|
||||||
|
|
||||||
**Use this guide to deploy ANY Node.js application to a TurnKey Linux LXC Container.**
|
**Use this guide to deploy ANY Node.js application to a TurnKey Linux LXC Container.**
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
## 📋 Prerequisites
|
## 📋 Prerequisites
|
||||||
|
|
||||||
1. **Project**: A Node.js application (Express, Next.js, etc.) in a Git repository.
|
1. **Project**: A Node.js application (Express, Next.js, etc.) in a Git repository.
|
||||||
2. **Server**: A Proxmox TurnKey Node.js Container.
|
2. **Server**: A Proxmox TurnKey Node.js Container.
|
||||||
3. **Access**: Root SSH password for the container.
|
3. **Access**: Root SSH password for the container.
|
||||||
4. **Domain (Optional)**: If using Cloudflare Tunnel.
|
4. **Domain (Optional)**: If using Cloudflare Tunnel.
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
## 🛠️ Step 1: Prepare Your Project
|
## 🛠️ Step 1: Prepare Your Project
|
||||||
|
|
||||||
Ensure your project is ready for production:
|
Ensure your project is ready for production:
|
||||||
|
|
||||||
1. **Port Configuration**: Ensure your app listens on a configurable port or a fixed internal port (e.g., `4001`).
|
1. **Port Configuration**: Ensure your app listens on a configurable port or a fixed internal port (e.g., `4001`).
|
||||||
```javascript
|
```javascript
|
||||||
// server.js
|
// server.js
|
||||||
const PORT = process.env.PORT || 4001;
|
const PORT = process.env.PORT || 4001;
|
||||||
app.listen(PORT, ...);
|
app.listen(PORT, ...);
|
||||||
```
|
```
|
||||||
|
|
||||||
2. **Git Ignore**: Ensure `node_modules` and config files with secrets are ignored.
|
2. **Git Ignore**: Ensure `node_modules` and config files with secrets are ignored.
|
||||||
```gitignore
|
```gitignore
|
||||||
node_modules/
|
node_modules/
|
||||||
.env
|
.env
|
||||||
config.json
|
config.json
|
||||||
```
|
```
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
## 🖥️ Step 2: One-Time Server Setup
|
## 🖥️ Step 2: One-Time Server Setup
|
||||||
|
|
||||||
SSH into your new container:
|
SSH into your new container:
|
||||||
```bash
|
```bash
|
||||||
ssh root@<YOUR_SERVER_IP>
|
ssh root@<YOUR_SERVER_IP>
|
||||||
```
|
```
|
||||||
|
|
||||||
Run these commands to prepare the environment:
|
Run these commands to prepare the environment:
|
||||||
|
|
||||||
### 1. Install Essentials
|
### 1. Install Essentials
|
||||||
```bash
|
```bash
|
||||||
apt-get update && apt-get install -y git
|
apt-get update && apt-get install -y git
|
||||||
```
|
```
|
||||||
|
|
||||||
### 2. Prepare Directory
|
### 2. Prepare Directory
|
||||||
```bash
|
```bash
|
||||||
# Standard web directory
|
# Standard web directory
|
||||||
mkdir -p /var/www/<APP_NAME>
|
mkdir -p /var/www/<APP_NAME>
|
||||||
cd /var/www/<APP_NAME>
|
cd /var/www/<APP_NAME>
|
||||||
|
|
||||||
# Clone your repo (Use Basic Auth with Token if private)
|
# Clone your repo (Use Basic Auth with Token if private)
|
||||||
# Format: https://<USER>:<TOKEN>@github.com/<ORG>/<REPO>.git
|
# Format: https://<USER>:<TOKEN>@github.com/<ORG>/<REPO>.git
|
||||||
git clone <YOUR_REPO_URL> .
|
git clone <YOUR_REPO_URL> .
|
||||||
|
|
||||||
# Install dependencies
|
# Install dependencies
|
||||||
npm install
|
npm install
|
||||||
```
|
```
|
||||||
|
|
||||||
### 3. Setup Permissions
|
### 3. Setup Permissions
|
||||||
```bash
|
```bash
|
||||||
# Give ownership to www-data (Nginx user)
|
# Give ownership to www-data (Nginx user)
|
||||||
chown -R www-data:www-data /var/www/<APP_NAME>
|
chown -R www-data:www-data /var/www/<APP_NAME>
|
||||||
```
|
```
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
## ⚙️ Step 3: Application Configuration
|
## ⚙️ Step 3: Application Configuration
|
||||||
|
|
||||||
### 1. Systemd Service
|
### 1. Systemd Service
|
||||||
Create a service file to keep your app running.
|
Create a service file to keep your app running.
|
||||||
|
|
||||||
Create `/etc/systemd/system/<APP_NAME>.service`:
|
Create `/etc/systemd/system/<APP_NAME>.service`:
|
||||||
```ini
|
```ini
|
||||||
[Unit]
|
[Unit]
|
||||||
Description=<APP_NAME> Service
|
Description=<APP_NAME> Service
|
||||||
After=network.target
|
After=network.target
|
||||||
|
|
||||||
[Service]
|
[Service]
|
||||||
Type=simple
|
Type=simple
|
||||||
User=root
|
User=root
|
||||||
# OR use 'www-data' if app doesn't need root ports
|
# OR use 'www-data' if app doesn't need root ports
|
||||||
# User=www-data
|
# User=www-data
|
||||||
WorkingDirectory=/var/www/<APP_NAME>
|
WorkingDirectory=/var/www/<APP_NAME>
|
||||||
ExecStart=/usr/local/bin/node server.js
|
ExecStart=/usr/local/bin/node server.js
|
||||||
Restart=always
|
Restart=always
|
||||||
Environment=NODE_ENV=production
|
Environment=NODE_ENV=production
|
||||||
Environment=PORT=4001
|
Environment=PORT=4001
|
||||||
|
|
||||||
[Install]
|
[Install]
|
||||||
WantedBy=multi-user.target
|
WantedBy=multi-user.target
|
||||||
```
|
```
|
||||||
|
|
||||||
Enable and start:
|
Enable and start:
|
||||||
```bash
|
```bash
|
||||||
systemctl daemon-reload
|
systemctl daemon-reload
|
||||||
systemctl enable <APP_NAME>
|
systemctl enable <APP_NAME>
|
||||||
systemctl start <APP_NAME>
|
systemctl start <APP_NAME>
|
||||||
```
|
```
|
||||||
|
|
||||||
### 2. Nginx Reverse Proxy
|
### 2. Nginx Reverse Proxy
|
||||||
Configure Nginx to forward port 80 to your app (Port 4001).
|
Configure Nginx to forward port 80 to your app (Port 4001).
|
||||||
|
|
||||||
Create `/etc/nginx/sites-available/<APP_NAME>`:
|
Create `/etc/nginx/sites-available/<APP_NAME>`:
|
||||||
```nginx
|
```nginx
|
||||||
server {
|
server {
|
||||||
listen 80;
|
listen 80;
|
||||||
server_name _;
|
server_name _;
|
||||||
|
|
||||||
root /var/www/<APP_NAME>;
|
root /var/www/<APP_NAME>;
|
||||||
index index.html;
|
index index.html;
|
||||||
|
|
||||||
# Serve static files (Optional)
|
# Serve static files (Optional)
|
||||||
location / {
|
location / {
|
||||||
try_files $uri $uri/ =404;
|
try_files $uri $uri/ =404;
|
||||||
}
|
}
|
||||||
|
|
||||||
# Proxy API/Dynamic requests
|
# Proxy API/Dynamic requests
|
||||||
location /api {
|
location /api {
|
||||||
proxy_pass http://localhost:4001;
|
proxy_pass http://localhost:4001;
|
||||||
proxy_http_version 1.1;
|
proxy_http_version 1.1;
|
||||||
proxy_set_header Upgrade $http_upgrade;
|
proxy_set_header Upgrade $http_upgrade;
|
||||||
proxy_set_header Connection 'upgrade';
|
proxy_set_header Connection 'upgrade';
|
||||||
proxy_set_header Host $host;
|
proxy_set_header Host $host;
|
||||||
proxy_cache_bypass $http_upgrade;
|
proxy_cache_bypass $http_upgrade;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
```
|
```
|
||||||
|
|
||||||
Enable site:
|
Enable site:
|
||||||
```bash
|
```bash
|
||||||
# Remove defaults
|
# Remove defaults
|
||||||
rm -f /etc/nginx/sites-enabled/default
|
rm -f /etc/nginx/sites-enabled/default
|
||||||
rm -f /etc/nginx/sites-enabled/nodejs
|
rm -f /etc/nginx/sites-enabled/nodejs
|
||||||
|
|
||||||
# Link new site
|
# Link new site
|
||||||
ln -s /etc/nginx/sites-available/<APP_NAME> /etc/nginx/sites-enabled/
|
ln -s /etc/nginx/sites-available/<APP_NAME> /etc/nginx/sites-enabled/
|
||||||
|
|
||||||
# Reload
|
# Reload
|
||||||
nginx -t && systemctl reload nginx
|
nginx -t && systemctl reload nginx
|
||||||
```
|
```
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
## ☁️ Step 4: Cloudflare Tunnel (Secure Access)
|
## ☁️ Step 4: Cloudflare Tunnel (Secure Access)
|
||||||
|
|
||||||
Expose your app securely without opening router ports.
|
Expose your app securely without opening router ports.
|
||||||
|
|
||||||
### 1. Install Cloudflared
|
### 1. Install Cloudflared
|
||||||
```bash
|
```bash
|
||||||
# Add Key
|
# Add Key
|
||||||
mkdir -p --mode=0755 /usr/share/keyrings
|
mkdir -p --mode=0755 /usr/share/keyrings
|
||||||
curl -fsSL https://pkg.cloudflare.com/cloudflare-public-v2.gpg | tee /usr/share/keyrings/cloudflare-public-v2.gpg >/dev/null
|
curl -fsSL https://pkg.cloudflare.com/cloudflare-public-v2.gpg | tee /usr/share/keyrings/cloudflare-public-v2.gpg >/dev/null
|
||||||
|
|
||||||
# Add Repo
|
# Add Repo
|
||||||
echo 'deb [signed-by=/usr/share/keyrings/cloudflare-public-v2.gpg] https://pkg.cloudflare.com/cloudflared any main' | tee /etc/apt/sources.list.d/cloudflared.list
|
echo 'deb [signed-by=/usr/share/keyrings/cloudflare-public-v2.gpg] https://pkg.cloudflare.com/cloudflared any main' | tee /etc/apt/sources.list.d/cloudflared.list
|
||||||
|
|
||||||
# Install
|
# Install
|
||||||
apt-get update && apt-get install -y cloudflared
|
apt-get update && apt-get install -y cloudflared
|
||||||
```
|
```
|
||||||
|
|
||||||
### 2. Create Tunnel
|
### 2. Create Tunnel
|
||||||
```bash
|
```bash
|
||||||
cloudflared tunnel login
|
cloudflared tunnel login
|
||||||
cloudflared tunnel create <TUNNEL_NAME>
|
cloudflared tunnel create <TUNNEL_NAME>
|
||||||
# Follow on-screen instructions to map domain -> http://localhost:4001
|
# Follow on-screen instructions to map domain -> http://localhost:4001
|
||||||
```
|
```
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
## 🔄 Step 5: Automated Updates (PowerShell)
|
## 🔄 Step 5: Automated Updates (PowerShell)
|
||||||
|
|
||||||
Create a script `deploy-remote.ps1` in your project root to automate updates.
|
Create a script `deploy-remote.ps1` in your project root to automate updates.
|
||||||
|
|
||||||
**Pre-requisite**: Create `deploy-config.json` (Add to .gitignore!):
|
**Pre-requisite**: Create `deploy-config.json` (Add to .gitignore!):
|
||||||
```json
|
```json
|
||||||
{
|
{
|
||||||
"host": "<SERVER_IP>",
|
"host": "<SERVER_IP>",
|
||||||
"username": "root",
|
"username": "root",
|
||||||
"password": "<SSH_PASSWORD>",
|
"password": "<SSH_PASSWORD>",
|
||||||
"remotePath": "/var/www/<APP_NAME>"
|
"remotePath": "/var/www/<APP_NAME>"
|
||||||
}
|
}
|
||||||
```
|
```
|
||||||
|
|
||||||
**Script `deploy-remote.ps1`**:
|
**Script `deploy-remote.ps1`**:
|
||||||
```powershell
|
```powershell
|
||||||
# Reads config and updates remote server
|
# Reads config and updates remote server
|
||||||
$Config = Get-Content "deploy-config.json" | ConvertFrom-Json
|
$Config = Get-Content "deploy-config.json" | ConvertFrom-Json
|
||||||
$User = $Config.username; $HostName = $Config.host; $Pass = $Config.password
|
$User = $Config.username; $HostName = $Config.host; $Pass = $Config.password
|
||||||
$RemotePath = $Config.remotePath
|
$RemotePath = $Config.remotePath
|
||||||
|
|
||||||
# Commands to run remotely
|
# Commands to run remotely
|
||||||
$Cmds = "
|
$Cmds = "
|
||||||
cd $RemotePath
|
cd $RemotePath
|
||||||
echo '⬇️ Pulling code...'
|
echo '⬇️ Pulling code...'
|
||||||
git pull
|
git pull
|
||||||
echo '📦 Installing deps...'
|
echo '📦 Installing deps...'
|
||||||
npm install
|
npm install
|
||||||
echo '🚀 Restarting service...'
|
echo '🚀 Restarting service...'
|
||||||
systemctl restart <APP_NAME>
|
systemctl restart <APP_NAME>
|
||||||
systemctl status <APP_NAME> --no-pager
|
systemctl status <APP_NAME> --no-pager
|
||||||
"
|
"
|
||||||
|
|
||||||
echo y | plink -ssh -t -pw $Pass "$User@$HostName" $Cmds
|
echo y | plink -ssh -t -pw $Pass "$User@$HostName" $Cmds
|
||||||
```
|
```
|
||||||
|
|
||||||
**Usage**: Just run `./deploy-remote.ps1` to deploy!
|
**Usage**: Just run `./deploy-remote.ps1` to deploy!
|
||||||
|
|||||||
250
README.md
250
README.md
@@ -1,125 +1,125 @@
|
|||||||
# Beyond Cloud Technology - Website Stress Test
|
# Beyond Cloud Technology - Website Stress Test
|
||||||
|
|
||||||

|

|
||||||
|
|
||||||
## 🌐 Public Access
|
## 🌐 Public Access
|
||||||
**Live URL:** [https://website-stress-test.beyondcloud.technology/](https://website-stress-test.beyondcloud.technology/)
|
**Live URL:** [https://website-stress-test.beyondcloud.technology/](https://website-stress-test.beyondcloud.technology/)
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
## 🚀 Overview
|
## 🚀 Overview
|
||||||
The **Website Stress Test** is a professional-grade load testing tool designed to simulate realistic traffic patterns on your web applications. It helps developers and QA engineers identify bottlenecks, test scalability, and ensure production readiness.
|
The **Website Stress Test** is a professional-grade load testing tool designed to simulate realistic traffic patterns on your web applications. It helps developers and QA engineers identify bottlenecks, test scalability, and ensure production readiness.
|
||||||
|
|
||||||
Built with a **modern, high-performance architecture**, it includes a custom NodeJS proxy server to bypass CORS restrictions and allow testing of any target URL.
|
Built with a **modern, high-performance architecture**, it includes a custom NodeJS proxy server to bypass CORS restrictions and allow testing of any target URL.
|
||||||
|
|
||||||
## ✨ Key Features
|
## ✨ Key Features
|
||||||
|
|
||||||
### 🛠️ Core Functionality
|
### 🛠️ Core Functionality
|
||||||
* **Custom HTTP Methods**: Support for GET, POST, PUT, DELETE, and PATCH.
|
* **Custom HTTP Methods**: Support for GET, POST, PUT, DELETE, and PATCH.
|
||||||
* **Configurable Load**: Adjust concurrent users (up to 5000) and test duration.
|
* **Configurable Load**: Adjust concurrent users (up to 5000) and test duration.
|
||||||
* **Traffic Patterns**:
|
* **Traffic Patterns**:
|
||||||
* **Steady**: Constant load.
|
* **Steady**: Constant load.
|
||||||
* **Burst**: Sudden spikes to test resilience.
|
* **Burst**: Sudden spikes to test resilience.
|
||||||
* **Ramp-up**: Gradual increase to find breaking points.
|
* **Ramp-up**: Gradual increase to find breaking points.
|
||||||
* **Random**: Simulate unpredictable real-world traffic.
|
* **Random**: Simulate unpredictable real-world traffic.
|
||||||
* **Crawler Mode**: Automatically crawls the target website to test multiple pages and paths, not just the entry point.
|
* **Crawler Mode**: Automatically crawls the target website to test multiple pages and paths, not just the entry point.
|
||||||
|
|
||||||
### 📊 Real-Time Analytics
|
### 📊 Real-Time Analytics
|
||||||
* **Interactive Charts**: Live visualization of Requests Per Second (RPS) and Response Times.
|
* **Interactive Charts**: Live visualization of Requests Per Second (RPS) and Response Times.
|
||||||
* **Detailed Metrics**: Track Active Users, Bandwidth, Success Rates, and Error breakdown (4xx, 5xx, Timeouts).
|
* **Detailed Metrics**: Track Active Users, Bandwidth, Success Rates, and Error breakdown (4xx, 5xx, Timeouts).
|
||||||
* **Percentiles**: Monitor P50, P95, and P99 latency metrics.
|
* **Percentiles**: Monitor P50, P95, and P99 latency metrics.
|
||||||
|
|
||||||
### 🎨 User Experience
|
### 🎨 User Experience
|
||||||
* **Modern UI**: Sleek, glassmorphism-inspired design with Light/Dark mode support.
|
* **Modern UI**: Sleek, glassmorphism-inspired design with Light/Dark mode support.
|
||||||
* **Git Versioning**: Automatic display of the current Git Commit ID and deployment age in the UI.
|
* **Git Versioning**: Automatic display of the current Git Commit ID and deployment age in the UI.
|
||||||
* **Responsive Design**: Fully functional on desktop and tablet devices.
|
* **Responsive Design**: Fully functional on desktop and tablet devices.
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
## 📦 Installation & Setup
|
## 📦 Installation & Setup
|
||||||
|
|
||||||
### Prerequisites
|
### Prerequisites
|
||||||
* Node.js (v18 or higher)
|
* Node.js (v18 or higher)
|
||||||
* Nginx (for production deployment)
|
* Nginx (for production deployment)
|
||||||
* PM2 (for process management)
|
* PM2 (for process management)
|
||||||
|
|
||||||
### 💻 Local Development
|
### 💻 Local Development
|
||||||
1. **Clone the Repository**
|
1. **Clone the Repository**
|
||||||
```bash
|
```bash
|
||||||
git clone https://github.com/DeNNiiInc/Website-Stress-Test.git
|
git clone https://github.com/DeNNiiInc/Website-Stress-Test.git
|
||||||
cd Website-Stress-Test
|
cd Website-Stress-Test
|
||||||
```
|
```
|
||||||
|
|
||||||
2. **Install Dependencies**
|
2. **Install Dependencies**
|
||||||
```bash
|
```bash
|
||||||
npm install
|
npm install
|
||||||
```
|
```
|
||||||
|
|
||||||
3. **Start the Proxy Server**
|
3. **Start the Proxy Server**
|
||||||
```bash
|
```bash
|
||||||
npm start
|
npm start
|
||||||
```
|
```
|
||||||
The server will start on `http://localhost:3000`.
|
The server will start on `http://localhost:3000`.
|
||||||
|
|
||||||
4. **Open the Application**
|
4. **Open the Application**
|
||||||
Open `index.html` in your browser or serve it using a static file server (e.g., Live Server).
|
Open `index.html` in your browser or serve it using a static file server (e.g., Live Server).
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
## 🚀 Deployment Guide (Proxmox / Ubuntu)
|
## 🚀 Deployment Guide (Proxmox / Ubuntu)
|
||||||
|
|
||||||
This project includes automated deployment scripts for Proxmox/Ubuntu environments.
|
This project includes automated deployment scripts for Proxmox/Ubuntu environments.
|
||||||
|
|
||||||
### 1. Configuration
|
### 1. Configuration
|
||||||
Copy `deploy-config.example.json` to `deploy-config.json` and update with your server details:
|
Copy `deploy-config.example.json` to `deploy-config.json` and update with your server details:
|
||||||
```json
|
```json
|
||||||
{
|
{
|
||||||
"host": "YOUR_SERVER_IP",
|
"host": "YOUR_SERVER_IP",
|
||||||
"username": "root",
|
"username": "root",
|
||||||
"password": "YOUR_PASSWORD",
|
"password": "YOUR_PASSWORD",
|
||||||
"remotePath": "/var/www/website-stress-test",
|
"remotePath": "/var/www/website-stress-test",
|
||||||
"repoUrl": "https://github.com/DeNNiiInc/Website-Stress-Test.git",
|
"repoUrl": "https://github.com/DeNNiiInc/Website-Stress-Test.git",
|
||||||
"githubToken": "YOUR_GITHUB_TOKEN",
|
"githubToken": "YOUR_GITHUB_TOKEN",
|
||||||
"appName": "website-stress-test"
|
"appName": "website-stress-test"
|
||||||
}
|
}
|
||||||
```
|
```
|
||||||
|
|
||||||
### 2. Auto-Deployment
|
### 2. Auto-Deployment
|
||||||
Run the PowerShell deployment script:
|
Run the PowerShell deployment script:
|
||||||
```powershell
|
```powershell
|
||||||
./start-deployment.ps1
|
./start-deployment.ps1
|
||||||
```
|
```
|
||||||
This script will:
|
This script will:
|
||||||
* Connect to your server via SSH.
|
* Connect to your server via SSH.
|
||||||
* Install Nginx and Node.js if missing.
|
* Install Nginx and Node.js if missing.
|
||||||
* Clone/Pull the latest code.
|
* Clone/Pull the latest code.
|
||||||
* Configure Nginx as a reverse proxy.
|
* Configure Nginx as a reverse proxy.
|
||||||
* Set up a Cron job for auto-updates.
|
* Set up a Cron job for auto-updates.
|
||||||
|
|
||||||
### 3. Auto-Sync
|
### 3. Auto-Sync
|
||||||
The system automatically checks for Git updates every 5 minutes. If changes are detected, it pulls the code, installs dependencies, and restarts the backend process without downtime.
|
The system automatically checks for Git updates every 5 minutes. If changes are detected, it pulls the code, installs dependencies, and restarts the backend process without downtime.
|
||||||
|
|
||||||
**Manual Update Trigger:**
|
**Manual Update Trigger:**
|
||||||
```bash
|
```bash
|
||||||
/var/www/website-stress-test/auto-sync.sh
|
/var/www/website-stress-test/auto-sync.sh
|
||||||
```
|
```
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
## 🔧 Architecture
|
## 🔧 Architecture
|
||||||
|
|
||||||
### Backend (`proxy-server.js`)
|
### Backend (`proxy-server.js`)
|
||||||
* **Role**: Handles CORS requests and authenticates traffic.
|
* **Role**: Handles CORS requests and authenticates traffic.
|
||||||
* **Port**: 3000 (Internal).
|
* **Port**: 3000 (Internal).
|
||||||
* **Endpoints**:
|
* **Endpoints**:
|
||||||
* `/proxy`: Forwards stress test requests.
|
* `/proxy`: Forwards stress test requests.
|
||||||
* `/git-info`: Returns current commit hash and deployment date.
|
* `/git-info`: Returns current commit hash and deployment date.
|
||||||
|
|
||||||
### Frontend (`index.html` + `script.js`)
|
### Frontend (`index.html` + `script.js`)
|
||||||
* **Technology**: Vanilla JS + Chart.js.
|
* **Technology**: Vanilla JS + Chart.js.
|
||||||
* **Communication**: Fetch API to the Proxy Server.
|
* **Communication**: Fetch API to the Proxy Server.
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
## 📝 License
|
## 📝 License
|
||||||
MIT License - Copyright (c) 2025 Beyond Cloud Technology.
|
MIT License - Copyright (c) 2025 Beyond Cloud Technology.
|
||||||
|
|||||||
@@ -1,9 +1,9 @@
|
|||||||
{
|
{
|
||||||
"host": "YOUR_SERVER_IP",
|
"host": "YOUR_SERVER_IP",
|
||||||
"username": "root",
|
"username": "root",
|
||||||
"password": "YOUR_SSH_PASSWORD",
|
"password": "YOUR_SSH_PASSWORD",
|
||||||
"remotePath": "/var/www/website-stress-test",
|
"remotePath": "/var/www/website-stress-test",
|
||||||
"repoUrl": "https://github.com/DeNNiiInc/Website-Stress-Test.git",
|
"repoUrl": "https://github.com/DeNNiiInc/Website-Stress-Test.git",
|
||||||
"githubToken": "YOUR_GITHUB_TOKEN",
|
"githubToken": "YOUR_GITHUB_TOKEN",
|
||||||
"appName": "website-stress-test"
|
"appName": "website-stress-test"
|
||||||
}
|
}
|
||||||
|
|||||||
1024
index.html
1024
index.html
File diff suppressed because it is too large
Load Diff
34
package.json
34
package.json
@@ -1,17 +1,17 @@
|
|||||||
{
|
{
|
||||||
"name": "stress-testing-tool",
|
"name": "stress-testing-tool",
|
||||||
"version": "1.0.0",
|
"version": "1.0.0",
|
||||||
"description": "Website stress testing tool with CORS proxy",
|
"description": "Website stress testing tool with CORS proxy",
|
||||||
"main": "proxy-server.js",
|
"main": "proxy-server.js",
|
||||||
"scripts": {
|
"scripts": {
|
||||||
"start": "node proxy-server.js",
|
"start": "node proxy-server.js",
|
||||||
"proxy": "node proxy-server.js"
|
"proxy": "node proxy-server.js"
|
||||||
},
|
},
|
||||||
"keywords": [
|
"keywords": [
|
||||||
"stress-testing",
|
"stress-testing",
|
||||||
"load-testing",
|
"load-testing",
|
||||||
"cors-proxy"
|
"cors-proxy"
|
||||||
],
|
],
|
||||||
"author": "",
|
"author": "",
|
||||||
"license": "MIT"
|
"license": "MIT"
|
||||||
}
|
}
|
||||||
|
|||||||
564
proxy-server.js
564
proxy-server.js
@@ -1,273 +1,291 @@
|
|||||||
// ===================================
|
// ===================================
|
||||||
// CORS PROXY SERVER
|
// CORS PROXY SERVER
|
||||||
// ===================================
|
// ===================================
|
||||||
// This proxy server allows the stress testing tool to test
|
// This proxy server allows the stress testing tool to test
|
||||||
// production websites without CORS restrictions.
|
// production websites without CORS restrictions.
|
||||||
|
|
||||||
const http = require('http');
|
const http = require('http');
|
||||||
const https = require('https');
|
const https = require('https');
|
||||||
const url = require('url');
|
const url = require('url');
|
||||||
|
const cluster = require('cluster');
|
||||||
const PORT = 3000;
|
const numCPUs = require('os').cpus().length;
|
||||||
|
|
||||||
// Configuration
|
const PORT = process.env.PORT || 3000;
|
||||||
const CONFIG = {
|
|
||||||
// Maximum request timeout (30 seconds)
|
// Configuration
|
||||||
timeout: 30000,
|
const CONFIG = {
|
||||||
|
// Maximum request timeout (30 seconds)
|
||||||
// Allowed origins (restrict to your stress testing tool's domain)
|
timeout: 30000,
|
||||||
// Use '*' for development, specific domain for production
|
|
||||||
allowedOrigins: '*',
|
// Allowed origins (restrict to your stress testing tool's domain)
|
||||||
|
// Use '*' for development, specific domain for production
|
||||||
// Maximum concurrent connections
|
allowedOrigins: '*',
|
||||||
maxConnections: 5000,
|
|
||||||
|
// Maximum concurrent connections
|
||||||
// User agents for rotation
|
maxConnections: 10000, // Increased for cluster
|
||||||
userAgents: [
|
|
||||||
'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/120.0.0.0 Safari/537.36',
|
// User agents for rotation
|
||||||
'Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/120.0.0.0 Safari/537.36',
|
userAgents: [
|
||||||
'Mozilla/5.0 (Windows NT 10.0; Win64; x64; rv:121.0) Gecko/20100101 Firefox/121.0',
|
'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/120.0.0.0 Safari/537.36',
|
||||||
'Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/605.1.15 (KHTML, like Gecko) Version/17.1 Safari/605.1.15',
|
'Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/120.0.0.0 Safari/537.36',
|
||||||
'Mozilla/5.0 (X11; Linux x86_64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/120.0.0.0 Safari/537.36'
|
'Mozilla/5.0 (Windows NT 10.0; Win64; x64; rv:121.0) Gecko/20100101 Firefox/121.0',
|
||||||
]
|
'Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/605.1.15 (KHTML, like Gecko) Version/17.1 Safari/605.1.15',
|
||||||
};
|
'Mozilla/5.0 (X11; Linux x86_64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/120.0.0.0 Safari/537.36'
|
||||||
|
]
|
||||||
// Get random user agent
|
};
|
||||||
function getRandomUserAgent() {
|
|
||||||
return CONFIG.userAgents[Math.floor(Math.random() * CONFIG.userAgents.length)];
|
// Global agents for connection pooling
|
||||||
}
|
const httpAgent = new http.Agent({ keepAlive: true, maxSockets: Infinity });
|
||||||
|
const httpsAgent = new https.Agent({ keepAlive: true, maxSockets: Infinity });
|
||||||
const { exec } = require('child_process');
|
|
||||||
|
// Get random user agent
|
||||||
// Helper to get git info
|
function getRandomUserAgent() {
|
||||||
const getGitInfo = () => {
|
return CONFIG.userAgents[Math.floor(Math.random() * CONFIG.userAgents.length)];
|
||||||
return new Promise((resolve) => {
|
}
|
||||||
exec('git rev-parse --short HEAD && git log -1 --format=%cd --date=relative', (err, stdout) => {
|
|
||||||
if (err) {
|
const { exec } = require('child_process');
|
||||||
console.error('Error fetching git info:', err);
|
|
||||||
resolve({ commit: 'Unknown', date: 'Unknown' });
|
// Helper to get git info
|
||||||
return;
|
const getGitInfo = () => {
|
||||||
}
|
return new Promise((resolve) => {
|
||||||
const parts = stdout.trim().split('\n');
|
exec('git rev-parse --short HEAD && git log -1 --format=%cd --date=relative', (err, stdout) => {
|
||||||
resolve({
|
if (err) {
|
||||||
commit: parts[0] || 'Unknown',
|
resolve({ commit: 'Unknown', date: 'Unknown' });
|
||||||
date: parts[1] || 'Unknown'
|
return;
|
||||||
});
|
}
|
||||||
});
|
const parts = stdout.trim().split('\n');
|
||||||
});
|
resolve({
|
||||||
};
|
commit: parts[0] || 'Unknown',
|
||||||
|
date: parts[1] || 'Unknown'
|
||||||
// Create the proxy server
|
});
|
||||||
const server = http.createServer((req, res) => {
|
});
|
||||||
// Handle CORS preflight requests
|
});
|
||||||
if (req.method === 'OPTIONS') {
|
};
|
||||||
handleCORS(res);
|
|
||||||
res.writeHead(200);
|
if (cluster.isMaster) {
|
||||||
res.end();
|
console.log(`Master ${process.pid} is running`);
|
||||||
return;
|
console.log(`Spawning ${numCPUs} workers...`);
|
||||||
}
|
|
||||||
|
for (let i = 0; i < numCPUs; i++) {
|
||||||
// Handle Git Info request
|
cluster.fork();
|
||||||
// Nginx proxy_pass might result in double slashes (//git-info)
|
}
|
||||||
if ((req.url === '/git-info' || req.url === '//git-info') && req.method === 'GET') {
|
|
||||||
handleCORS(res);
|
cluster.on('exit', (worker, code, signal) => {
|
||||||
getGitInfo().then(info => {
|
console.log(`Worker ${worker.process.pid} died. Respawning...`);
|
||||||
res.writeHead(200, { 'Content-Type': 'application/json' });
|
cluster.fork();
|
||||||
res.end(JSON.stringify(info));
|
});
|
||||||
});
|
|
||||||
return;
|
// Master process only listens for SIGINT to gracefully shut down workers
|
||||||
}
|
process.on('SIGINT', () => {
|
||||||
|
console.log('\n\n🛑 Shutting down proxy server (master)...');
|
||||||
// Only allow POST requests to the proxy
|
for (const id in cluster.workers) {
|
||||||
if (req.method !== 'POST') {
|
cluster.workers[id].kill();
|
||||||
res.writeHead(405, { 'Content-Type': 'application/json' });
|
}
|
||||||
res.end(JSON.stringify({ error: 'Method not allowed. Use POST.' }));
|
process.exit(0);
|
||||||
return;
|
});
|
||||||
}
|
|
||||||
|
} else {
|
||||||
// Parse request body
|
// Create the proxy server
|
||||||
let body = '';
|
const server = http.createServer((req, res) => {
|
||||||
req.on('data', chunk => {
|
// Handle CORS preflight requests
|
||||||
body += chunk.toString();
|
if (req.method === 'OPTIONS') {
|
||||||
});
|
handleCORS(res);
|
||||||
|
res.writeHead(200);
|
||||||
req.on('end', () => {
|
res.end();
|
||||||
try {
|
return;
|
||||||
const proxyRequest = JSON.parse(body);
|
}
|
||||||
handleProxyRequest(proxyRequest, res);
|
|
||||||
} catch (error) {
|
// Health check
|
||||||
res.writeHead(400, { 'Content-Type': 'application/json' });
|
if (req.url === '/health' || req.url === '//health') {
|
||||||
res.end(JSON.stringify({
|
handleCORS(res);
|
||||||
error: 'Invalid JSON',
|
res.writeHead(200, { 'Content-Type': 'application/json' });
|
||||||
message: error.message
|
res.end(JSON.stringify({ status: 'ok', worker: process.pid }));
|
||||||
}));
|
return;
|
||||||
}
|
}
|
||||||
});
|
|
||||||
});
|
// Handle Git Info request
|
||||||
|
// Nginx proxy_pass might result in double slashes (//git-info)
|
||||||
// Handle the actual proxy request
|
if ((req.url === '/git-info' || req.url === '//git-info') && req.method === 'GET') {
|
||||||
function handleProxyRequest(proxyRequest, clientRes) {
|
handleCORS(res);
|
||||||
const { targetUrl, method = 'GET', headers = {}, body = null } = proxyRequest;
|
getGitInfo().then(info => {
|
||||||
|
res.writeHead(200, { 'Content-Type': 'application/json' });
|
||||||
// Validate target URL
|
res.end(JSON.stringify(info));
|
||||||
if (!targetUrl) {
|
});
|
||||||
clientRes.writeHead(400, { 'Content-Type': 'application/json' });
|
return;
|
||||||
clientRes.end(JSON.stringify({ error: 'targetUrl is required' }));
|
}
|
||||||
return;
|
|
||||||
}
|
// Only allow POST requests to the proxy
|
||||||
|
if (req.method !== 'POST') {
|
||||||
let parsedUrl;
|
res.writeHead(405, { 'Content-Type': 'application/json' });
|
||||||
try {
|
res.end(JSON.stringify({ error: 'Method not allowed. Use POST.' }));
|
||||||
parsedUrl = new URL(targetUrl);
|
return;
|
||||||
} catch (error) {
|
}
|
||||||
clientRes.writeHead(400, { 'Content-Type': 'application/json' });
|
|
||||||
clientRes.end(JSON.stringify({ error: 'Invalid URL' }));
|
// Parse request body
|
||||||
return;
|
let body = '';
|
||||||
}
|
req.on('data', chunk => {
|
||||||
|
body += chunk.toString();
|
||||||
// Determine if we need http or https
|
});
|
||||||
const protocol = parsedUrl.protocol === 'https:' ? https : http;
|
|
||||||
|
req.on('end', () => {
|
||||||
// Prepare request options with random user agent
|
try {
|
||||||
const options = {
|
const proxyRequest = JSON.parse(body);
|
||||||
hostname: parsedUrl.hostname,
|
handleProxyRequest(proxyRequest, res);
|
||||||
port: parsedUrl.port,
|
} catch (error) {
|
||||||
path: parsedUrl.pathname + parsedUrl.search,
|
res.writeHead(400, { 'Content-Type': 'application/json' });
|
||||||
method: method,
|
res.end(JSON.stringify({
|
||||||
headers: {
|
error: 'Invalid JSON',
|
||||||
...headers,
|
message: error.message
|
||||||
'User-Agent': getRandomUserAgent(),
|
}));
|
||||||
'Accept': 'text/html,application/xhtml+xml,application/xml;q=0.9,image/webp,*/*;q=0.8',
|
}
|
||||||
'Accept-Language': 'en-US,en;q=0.5',
|
});
|
||||||
'Accept-Encoding': 'gzip, deflate, br',
|
});
|
||||||
'DNT': '1',
|
|
||||||
'Connection': 'keep-alive',
|
// Handle the actual proxy request
|
||||||
'Upgrade-Insecure-Requests': '1'
|
function handleProxyRequest(proxyRequest, clientRes) {
|
||||||
},
|
const { targetUrl, method = 'GET', headers = {}, body = null } = proxyRequest;
|
||||||
timeout: CONFIG.timeout
|
|
||||||
};
|
// Validate target URL
|
||||||
|
if (!targetUrl) {
|
||||||
const startTime = Date.now();
|
clientRes.writeHead(400, { 'Content-Type': 'application/json' });
|
||||||
|
clientRes.end(JSON.stringify({ error: 'targetUrl is required' }));
|
||||||
// Make the request to the target server
|
return;
|
||||||
const proxyReq = protocol.request(options, (proxyRes) => {
|
}
|
||||||
const responseTime = Date.now() - startTime;
|
|
||||||
|
let parsedUrl;
|
||||||
// Collect response data
|
try {
|
||||||
let responseData = '';
|
parsedUrl = new URL(targetUrl);
|
||||||
let responseSize = 0;
|
} catch (error) {
|
||||||
const maxBodySize = 500000; // 500KB limit for crawler
|
clientRes.writeHead(400, { 'Content-Type': 'application/json' });
|
||||||
|
clientRes.end(JSON.stringify({ error: 'Invalid URL' }));
|
||||||
proxyRes.on('data', chunk => {
|
return;
|
||||||
responseSize += chunk.length;
|
}
|
||||||
// Only collect body if under size limit (for crawler)
|
|
||||||
if (responseSize < maxBodySize) {
|
// Determine if we need http or https and which agent to use
|
||||||
responseData += chunk.toString();
|
const isHttps = parsedUrl.protocol === 'https:';
|
||||||
}
|
const protocol = isHttps ? https : http;
|
||||||
});
|
const agent = isHttps ? httpsAgent : httpAgent;
|
||||||
|
|
||||||
proxyRes.on('end', () => {
|
// Prepare request options with random user agent
|
||||||
// Send response back to client with CORS headers
|
const options = {
|
||||||
handleCORS(clientRes);
|
hostname: parsedUrl.hostname,
|
||||||
clientRes.writeHead(200, { 'Content-Type': 'application/json' });
|
port: parsedUrl.port,
|
||||||
|
path: parsedUrl.pathname + parsedUrl.search,
|
||||||
clientRes.end(JSON.stringify({
|
method: method,
|
||||||
success: true,
|
agent: agent, // Use the global agent for connection pooling
|
||||||
statusCode: proxyRes.statusCode,
|
headers: {
|
||||||
statusMessage: proxyRes.statusMessage,
|
...headers,
|
||||||
responseTime: responseTime,
|
'User-Agent': getRandomUserAgent(),
|
||||||
headers: proxyRes.headers,
|
'Accept': 'text/html,application/xhtml+xml,application/xml;q=0.9,image/webp,*/*;q=0.8',
|
||||||
body: responseData, // Full body for crawler link extraction
|
'Accept-Language': 'en-US,en;q=0.5',
|
||||||
bodySize: responseSize
|
'Accept-Encoding': 'gzip, deflate, br',
|
||||||
}));
|
'DNT': '1',
|
||||||
});
|
'Connection': 'keep-alive',
|
||||||
});
|
'Upgrade-Insecure-Requests': '1'
|
||||||
|
},
|
||||||
// Handle request errors
|
timeout: CONFIG.timeout
|
||||||
proxyReq.on('error', (error) => {
|
};
|
||||||
const responseTime = Date.now() - startTime;
|
|
||||||
|
const startTime = Date.now();
|
||||||
handleCORS(clientRes);
|
|
||||||
clientRes.writeHead(200, { 'Content-Type': 'application/json' });
|
// Make the request to the target server
|
||||||
|
const proxyReq = protocol.request(options, (proxyRes) => {
|
||||||
clientRes.end(JSON.stringify({
|
const responseTime = Date.now() - startTime;
|
||||||
success: false,
|
|
||||||
error: error.message,
|
// Collect response data
|
||||||
responseTime: responseTime,
|
let responseData = '';
|
||||||
statusCode: 0
|
let responseSize = 0;
|
||||||
}));
|
const maxBodySize = 500000; // 500KB limit for crawler
|
||||||
});
|
|
||||||
|
proxyRes.on('data', chunk => {
|
||||||
// Handle timeout
|
responseSize += chunk.length;
|
||||||
proxyReq.on('timeout', () => {
|
// Only collect body if under size limit (for crawler)
|
||||||
proxyReq.destroy();
|
if (responseSize < maxBodySize) {
|
||||||
const responseTime = Date.now() - startTime;
|
responseData += chunk.toString();
|
||||||
|
}
|
||||||
handleCORS(clientRes);
|
});
|
||||||
clientRes.writeHead(200, { 'Content-Type': 'application/json' });
|
|
||||||
|
proxyRes.on('end', () => {
|
||||||
clientRes.end(JSON.stringify({
|
// Send response back to client with CORS headers
|
||||||
success: false,
|
handleCORS(clientRes);
|
||||||
error: 'Request timeout',
|
clientRes.writeHead(200, { 'Content-Type': 'application/json' });
|
||||||
responseTime: responseTime,
|
|
||||||
statusCode: 0
|
clientRes.end(JSON.stringify({
|
||||||
}));
|
success: true,
|
||||||
});
|
statusCode: proxyRes.statusCode,
|
||||||
|
statusMessage: proxyRes.statusMessage,
|
||||||
// Send request body if present
|
responseTime: responseTime,
|
||||||
if (body && method !== 'GET' && method !== 'HEAD') {
|
headers: proxyRes.headers,
|
||||||
proxyReq.write(typeof body === 'string' ? body : JSON.stringify(body));
|
body: responseData, // Full body for crawler link extraction
|
||||||
}
|
bodySize: responseSize,
|
||||||
|
proxyWorker: process.pid // Add worker ID for debugging
|
||||||
proxyReq.end();
|
}));
|
||||||
}
|
});
|
||||||
|
});
|
||||||
// Add CORS headers to response
|
|
||||||
function handleCORS(res) {
|
// Handle request errors
|
||||||
res.setHeader('Access-Control-Allow-Origin', CONFIG.allowedOrigins);
|
proxyReq.on('error', (error) => {
|
||||||
res.setHeader('Access-Control-Allow-Methods', 'POST, GET, OPTIONS');
|
const responseTime = Date.now() - startTime;
|
||||||
res.setHeader('Access-Control-Allow-Headers', 'Content-Type');
|
|
||||||
}
|
handleCORS(clientRes);
|
||||||
|
clientRes.writeHead(200, { 'Content-Type': 'application/json' });
|
||||||
// Start the server
|
|
||||||
server.listen(PORT, () => {
|
clientRes.end(JSON.stringify({
|
||||||
console.log(`
|
success: false,
|
||||||
╔════════════════════════════════════════════════════════════╗
|
error: error.message,
|
||||||
║ CORS Proxy Server for Stress Testing Tool ║
|
responseTime: responseTime,
|
||||||
╚════════════════════════════════════════════════════════════╝
|
statusCode: 0
|
||||||
|
}));
|
||||||
✅ Server running on: http://localhost:${PORT}
|
});
|
||||||
✅ Max connections: ${CONFIG.maxConnections}
|
|
||||||
✅ Request timeout: ${CONFIG.timeout}ms
|
// Handle timeout
|
||||||
|
proxyReq.on('timeout', () => {
|
||||||
📝 Usage:
|
proxyReq.destroy();
|
||||||
POST to http://localhost:${PORT} with JSON body:
|
const responseTime = Date.now() - startTime;
|
||||||
{
|
|
||||||
"targetUrl": "https://example.com",
|
handleCORS(clientRes);
|
||||||
"method": "GET",
|
clientRes.writeHead(200, { 'Content-Type': 'application/json' });
|
||||||
"headers": {},
|
|
||||||
"body": null
|
clientRes.end(JSON.stringify({
|
||||||
}
|
success: false,
|
||||||
|
error: 'Request timeout',
|
||||||
🔒 Security Note:
|
responseTime: responseTime,
|
||||||
For production, update CONFIG.allowedOrigins to your
|
statusCode: 0
|
||||||
stress testing tool's domain (not '*')
|
}));
|
||||||
|
});
|
||||||
Press Ctrl+C to stop the server
|
|
||||||
`);
|
// Send request body if present
|
||||||
});
|
if (body && method !== 'GET' && method !== 'HEAD') {
|
||||||
|
proxyReq.write(typeof body === 'string' ? body : JSON.stringify(body));
|
||||||
// Handle server errors
|
}
|
||||||
server.on('error', (error) => {
|
|
||||||
console.error('❌ Server error:', error.message);
|
proxyReq.end();
|
||||||
process.exit(1);
|
}
|
||||||
});
|
|
||||||
|
// Add CORS headers to response
|
||||||
// Graceful shutdown
|
function handleCORS(res) {
|
||||||
process.on('SIGINT', () => {
|
res.setHeader('Access-Control-Allow-Origin', CONFIG.allowedOrigins);
|
||||||
console.log('\n\n🛑 Shutting down proxy server...');
|
res.setHeader('Access-Control-Allow-Methods', 'POST, GET, OPTIONS');
|
||||||
server.close(() => {
|
res.setHeader('Access-Control-Allow-Headers', 'Content-Type');
|
||||||
console.log('✅ Server closed');
|
}
|
||||||
process.exit(0);
|
|
||||||
});
|
// Start the server
|
||||||
});
|
server.listen(PORT, () => {
|
||||||
|
console.log(`Worker ${process.pid} running on http://localhost:${PORT}`);
|
||||||
|
});
|
||||||
|
|
||||||
|
// Handle server errors
|
||||||
|
server.on('error', (error) => {
|
||||||
|
console.error(`❌ Worker ${process.pid} server error:`, error.message);
|
||||||
|
process.exit(1);
|
||||||
|
});
|
||||||
|
|
||||||
|
// Graceful shutdown for workers
|
||||||
|
process.on('SIGINT', () => {
|
||||||
|
console.log(`\n\n🛑 Worker ${process.pid} shutting down...`);
|
||||||
|
server.close(() => {
|
||||||
|
console.log(`✅ Worker ${process.pid} closed`);
|
||||||
|
process.exit(0);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|||||||
@@ -1,23 +1,25 @@
|
|||||||
#!/bin/bash
|
|
||||||
|
|
||||||
# setup-server.sh - Initial Setup Script
|
# setup-server.sh - Initial Setup Script
|
||||||
|
|
||||||
# 1. Install Global Dependencies
|
# 1. System Tuning for High Concurrency
|
||||||
|
echo "Tuning system limits..."
|
||||||
|
# Increase max open files for high connection counts
|
||||||
|
if ! grep -q "soft nofile 65535" /etc/security/limits.conf; then
|
||||||
|
echo "* soft nofile 65535" >> /etc/security/limits.conf
|
||||||
|
echo "* hard nofile 65535" >> /etc/security/limits.conf
|
||||||
|
fi
|
||||||
|
# Apply limits to current session (for the rest of this script)
|
||||||
|
ulimit -n 65535
|
||||||
|
|
||||||
|
# 2. Install Global Dependencies
|
||||||
echo "Installing PM2..."
|
echo "Installing PM2..."
|
||||||
npm install -g pm2
|
npm install -g pm2
|
||||||
|
|
||||||
# 2. Clone Repository
|
# 3. Clone Repository
|
||||||
# Expects: REPO_URL, APP_DIR, GITHUB_TOKEN inside the script or env
|
# ... (rest of cloning logic)
|
||||||
# We'll use arguments passed to this script: $1=REPO_URL $2=APP_DIR $3=GITHUB_TOKEN
|
|
||||||
|
|
||||||
REPO_URL="$1"
|
REPO_URL="$1"
|
||||||
APP_DIR="$2"
|
APP_DIR="$2"
|
||||||
GITHUB_TOKEN="$3"
|
GITHUB_TOKEN="$3"
|
||||||
|
|
||||||
# Construct URL with token for auth
|
|
||||||
# Extract host and path from REPO_URL (assuming https://github.com/user/repo.git)
|
|
||||||
# We need to insert token: https://TOKEN@github.com/user/repo.git
|
|
||||||
# Simple replacement:
|
|
||||||
AUTH_REPO_URL="${REPO_URL/https:\/\//https:\/\/$GITHUB_TOKEN@}"
|
AUTH_REPO_URL="${REPO_URL/https:\/\//https:\/\/$GITHUB_TOKEN@}"
|
||||||
|
|
||||||
echo "Preparing application directory: $APP_DIR"
|
echo "Preparing application directory: $APP_DIR"
|
||||||
@@ -33,18 +35,20 @@ else
|
|||||||
cd "$APP_DIR"
|
cd "$APP_DIR"
|
||||||
fi
|
fi
|
||||||
|
|
||||||
# 3. Install App Dependencies
|
# 4. Install App Dependencies
|
||||||
echo "Installing application dependencies..."
|
echo "Installing application dependencies..."
|
||||||
npm install
|
npm install
|
||||||
|
|
||||||
# 4. Start Application with PM2
|
# 5. Start Application with PM2
|
||||||
APP_NAME="website-stress-test"
|
APP_NAME="website-stress-test"
|
||||||
echo "Starting application with PM2 ($APP_NAME)..."
|
echo "Starting application with PM2 ($APP_NAME)..."
|
||||||
pm2 start proxy-server.js --name "$APP_NAME" --watch --ignore-watch="node_modules"
|
# Using Node built-in clustering, but PM2 monitors the master
|
||||||
|
pm2 stop "$APP_NAME" || true
|
||||||
|
pm2 start proxy-server.js --name "$APP_NAME" --max-memory-restart 1G
|
||||||
pm2 save
|
pm2 save
|
||||||
pm2 startup | tail -n 1 | bash # Setup startup script
|
pm2 startup | tail -n 1 | bash # Setup startup script
|
||||||
|
|
||||||
# 5. Setup Cron Job for Auto-Sync
|
# 6. Setup Cron Job for Auto-Sync
|
||||||
echo "Setting up Cron Job for auto-sync..."
|
echo "Setting up Cron Job for auto-sync..."
|
||||||
SCRIPT_PATH="$APP_DIR/auto-sync.sh"
|
SCRIPT_PATH="$APP_DIR/auto-sync.sh"
|
||||||
chmod +x "$SCRIPT_PATH"
|
chmod +x "$SCRIPT_PATH"
|
||||||
@@ -52,5 +56,5 @@ chmod +x "$SCRIPT_PATH"
|
|||||||
# Add to crontab if not exists
|
# Add to crontab if not exists
|
||||||
(crontab -l 2>/dev/null; echo "*/5 * * * * $SCRIPT_PATH >> /var/log/app-sync.log 2>&1") | crontab -
|
(crontab -l 2>/dev/null; echo "*/5 * * * * $SCRIPT_PATH >> /var/log/app-sync.log 2>&1") | crontab -
|
||||||
|
|
||||||
echo "✅ Setup Complete! Application is running."
|
echo "✅ Setup Complete! Application is running with system optimizations."
|
||||||
pm2 status
|
pm2 status
|
||||||
|
|||||||
@@ -1,73 +1,73 @@
|
|||||||
# start-deployment.ps1
|
# start-deployment.ps1
|
||||||
# Automates the deployment by reading config, uploading scripts, and executing setup.
|
# Automates the deployment by reading config, uploading scripts, and executing setup.
|
||||||
|
|
||||||
$ErrorActionPreference = "Stop"
|
$ErrorActionPreference = "Stop"
|
||||||
|
|
||||||
$ConfigPath = "deploy-config.json"
|
$ConfigPath = "deploy-config.json"
|
||||||
|
|
||||||
if (-not (Test-Path $ConfigPath)) {
|
if (-not (Test-Path $ConfigPath)) {
|
||||||
Write-Error "Configuration file '$ConfigPath' not found. Please copy 'deploy-config.example.json' to '$ConfigPath' and fill in your details."
|
Write-Error "Configuration file '$ConfigPath' not found. Please copy 'deploy-config.example.json' to '$ConfigPath' and fill in your details."
|
||||||
}
|
}
|
||||||
|
|
||||||
$Config = Get-Content $ConfigPath | ConvertFrom-Json
|
$Config = Get-Content $ConfigPath | ConvertFrom-Json
|
||||||
|
|
||||||
# Validate Config
|
# Validate Config
|
||||||
$Required = @("host", "username", "password", "remotePath", "repoUrl", "githubToken")
|
$Required = @("host", "username", "password", "remotePath", "repoUrl", "githubToken")
|
||||||
foreach ($Key in $Required) {
|
foreach ($Key in $Required) {
|
||||||
if (-not $Config.$Key) {
|
if (-not $Config.$Key) {
|
||||||
Write-Error "Missing required config key: $Key"
|
Write-Error "Missing required config key: $Key"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
$User = $Config.username
|
$User = $Config.username
|
||||||
$HostName = $Config.host
|
$HostName = $Config.host
|
||||||
$Pass = $Config.password
|
$Pass = $Config.password
|
||||||
# Note: Using password directly in script is tricky with standard ssh/scp without key.
|
# Note: Using password directly in script is tricky with standard ssh/scp without key.
|
||||||
# We will check if 'sshpass' or 'plink' is available, or guide user to use keys.
|
# We will check if 'sshpass' or 'plink' is available, or guide user to use keys.
|
||||||
# Since the user specifically mentioned providing credentials, they might expect us to use them.
|
# Since the user specifically mentioned providing credentials, they might expect us to use them.
|
||||||
# The template used 'plink -pw $Pass'. We will stick to that if available, or warn.
|
# The template used 'plink -pw $Pass'. We will stick to that if available, or warn.
|
||||||
|
|
||||||
# Check for plink
|
# Check for plink
|
||||||
if (Get-Command "plink.exe" -ErrorAction SilentlyContinue) {
|
if (Get-Command "plink.exe" -ErrorAction SilentlyContinue) {
|
||||||
Write-Host "Using plink for connection..."
|
Write-Host "Using plink for connection..."
|
||||||
$UsePlink = $true
|
$UsePlink = $true
|
||||||
}
|
}
|
||||||
else {
|
else {
|
||||||
Write-Warning "plink.exe not found. Falling back to standard scp/ssh. You may be prompted for password multiple times."
|
Write-Warning "plink.exe not found. Falling back to standard scp/ssh. You may be prompted for password multiple times."
|
||||||
$UsePlink = $false
|
$UsePlink = $false
|
||||||
}
|
}
|
||||||
|
|
||||||
$RemoteTmp = "/tmp"
|
$RemoteTmp = "/tmp"
|
||||||
$SetupScript = "setup-server.sh"
|
$SetupScript = "setup-server.sh"
|
||||||
$SyncScript = "auto-sync.sh"
|
$SyncScript = "auto-sync.sh"
|
||||||
|
|
||||||
Write-Host "🚀 Starting Deployment to $HostName..."
|
Write-Host "🚀 Starting Deployment to $HostName..."
|
||||||
|
|
||||||
# 1. Upload Scripts
|
# 1. Upload Scripts
|
||||||
Write-Host "Uploading scripts..."
|
Write-Host "Uploading scripts..."
|
||||||
if ($UsePlink) {
|
if ($UsePlink) {
|
||||||
echo y | pscp -P 22 -pw $Pass $SetupScript "$User@$HostName`:$RemoteTmp/$SetupScript"
|
echo y | pscp -P 22 -pw $Pass $SetupScript "$User@$HostName`:$RemoteTmp/$SetupScript"
|
||||||
echo y | pscp -P 22 -pw $Pass $SyncScript "$User@$HostName`:$RemoteTmp/$SyncScript"
|
echo y | pscp -P 22 -pw $Pass $SyncScript "$User@$HostName`:$RemoteTmp/$SyncScript"
|
||||||
}
|
}
|
||||||
else {
|
else {
|
||||||
scp $SetupScript "$User@$HostName`:$RemoteTmp/$SetupScript"
|
scp $SetupScript "$User@$HostName`:$RemoteTmp/$SetupScript"
|
||||||
scp $SyncScript "$User@$HostName`:$RemoteTmp/$SyncScript"
|
scp $SyncScript "$User@$HostName`:$RemoteTmp/$SyncScript"
|
||||||
}
|
}
|
||||||
|
|
||||||
# 2. Execute Setup
|
# 2. Execute Setup
|
||||||
Write-Host "Executing setup on remote server..."
|
Write-Host "Executing setup on remote server..."
|
||||||
$AppDir = $Config.remotePath
|
$AppDir = $Config.remotePath
|
||||||
$Repo = $Config.repoUrl
|
$Repo = $Config.repoUrl
|
||||||
$Token = $Config.githubToken
|
$Token = $Config.githubToken
|
||||||
|
|
||||||
# Make scripts executable and run setup
|
# Make scripts executable and run setup
|
||||||
$RemoteCmd = "chmod +x $RemoteTmp/$SetupScript $RemoteTmp/$SyncScript; $RemoteTmp/$SetupScript '$Repo' '$AppDir' '$Token'; rm $RemoteTmp/$SetupScript"
|
$RemoteCmd = "chmod +x $RemoteTmp/$SetupScript $RemoteTmp/$SyncScript; $RemoteTmp/$SetupScript '$Repo' '$AppDir' '$Token'; rm $RemoteTmp/$SetupScript"
|
||||||
|
|
||||||
if ($UsePlink) {
|
if ($UsePlink) {
|
||||||
echo y | plink -ssh -P 22 -t -pw $Pass "$User@$HostName" $RemoteCmd
|
echo y | plink -ssh -P 22 -t -pw $Pass "$User@$HostName" $RemoteCmd
|
||||||
}
|
}
|
||||||
else {
|
else {
|
||||||
ssh -t "$User@$HostName" $RemoteCmd
|
ssh -t "$User@$HostName" $RemoteCmd
|
||||||
}
|
}
|
||||||
|
|
||||||
Write-Host "🎉 Deployment command sent!"
|
Write-Host "🎉 Deployment command sent!"
|
||||||
|
|||||||
2202
styles.css
2202
styles.css
File diff suppressed because it is too large
Load Diff
237
worker.js
Normal file
237
worker.js
Normal file
@@ -0,0 +1,237 @@
|
|||||||
|
// ===================================
|
||||||
|
// STRESS TESTING TOOL - WEB WORKER
|
||||||
|
// Handles request loops for a group of users
|
||||||
|
// ===================================
|
||||||
|
|
||||||
|
let config = {};
|
||||||
|
let state = {
|
||||||
|
active: false,
|
||||||
|
users: [],
|
||||||
|
startTime: 0,
|
||||||
|
totalRequests: 0,
|
||||||
|
successfulRequests: 0,
|
||||||
|
failedRequests: 0,
|
||||||
|
responseTimes: [],
|
||||||
|
bytesSent: 0,
|
||||||
|
bytesReceived: 0,
|
||||||
|
errorsByCategory: {
|
||||||
|
"4xx": 0,
|
||||||
|
"5xx": 0,
|
||||||
|
"timeout": 0,
|
||||||
|
"network": 0
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
// Listen for messages from the main thread
|
||||||
|
self.onmessage = function (e) {
|
||||||
|
const { type, data } = e.data;
|
||||||
|
|
||||||
|
switch (type) {
|
||||||
|
case 'INIT':
|
||||||
|
config = data.config;
|
||||||
|
break;
|
||||||
|
case 'START':
|
||||||
|
state.active = true;
|
||||||
|
state.startTime = Date.now();
|
||||||
|
startUsers(data.users);
|
||||||
|
break;
|
||||||
|
case 'STOP':
|
||||||
|
state.active = false;
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
async function startUsers(userIndices) {
|
||||||
|
const pattern = config.trafficPattern;
|
||||||
|
const totalDuration = config.duration * 1000;
|
||||||
|
|
||||||
|
for (const index of userIndices) {
|
||||||
|
if (!state.active) break;
|
||||||
|
|
||||||
|
const delay = calculateStartDelay(index, userIndices.length, pattern, totalDuration);
|
||||||
|
|
||||||
|
setTimeout(() => {
|
||||||
|
if (state.active) {
|
||||||
|
runUser(index);
|
||||||
|
}
|
||||||
|
}, delay);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Start reporting results periodically
|
||||||
|
const reportInterval = setInterval(() => {
|
||||||
|
if (!state.active) {
|
||||||
|
clearInterval(reportInterval);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
reportResults();
|
||||||
|
}, 500);
|
||||||
|
}
|
||||||
|
|
||||||
|
function calculateStartDelay(index, count, pattern, duration) {
|
||||||
|
switch (pattern) {
|
||||||
|
case 'steady':
|
||||||
|
return (index % count) * 100;
|
||||||
|
case 'burst':
|
||||||
|
const burstIndex = Math.floor((index % count) / (count / 5));
|
||||||
|
return burstIndex * (duration / 5);
|
||||||
|
case 'rampup':
|
||||||
|
return (index % count) * (duration / count);
|
||||||
|
case 'random':
|
||||||
|
return Math.random() * (duration / 2);
|
||||||
|
default:
|
||||||
|
return 0;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
async function runUser(id) {
|
||||||
|
const endTime = state.startTime + config.duration * 1000;
|
||||||
|
let currentUrl = config.targetUrl;
|
||||||
|
let crawlDepth = 0;
|
||||||
|
|
||||||
|
while (state.active && Date.now() < endTime) {
|
||||||
|
const result = await makeRequest(currentUrl);
|
||||||
|
|
||||||
|
// Report individual request for history log (sampled)
|
||||||
|
if (Math.random() < 0.1 || config.userCount < 50) {
|
||||||
|
self.postMessage({
|
||||||
|
type: 'LOG',
|
||||||
|
data: {
|
||||||
|
url: currentUrl,
|
||||||
|
status: result.status,
|
||||||
|
responseTime: result.responseTime,
|
||||||
|
success: result.success,
|
||||||
|
timestamp: new Date().toLocaleTimeString()
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
// Logic for crawler (simplified for worker)
|
||||||
|
if (config.crawlerEnabled && result.success && result.body && crawlDepth < config.crawlDepth) {
|
||||||
|
const nextUrl = extractRandomLink(result.body, currentUrl);
|
||||||
|
if (nextUrl) {
|
||||||
|
currentUrl = nextUrl;
|
||||||
|
crawlDepth++;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Think time with jitter
|
||||||
|
const jitter = 0.5 + Math.random(); // 50% to 150%
|
||||||
|
const sleepTime = config.thinkTime * jitter;
|
||||||
|
await new Promise(resolve => setTimeout(resolve, sleepTime));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
async function makeRequest(targetUrl) {
|
||||||
|
const startTime = performance.now();
|
||||||
|
let result = {
|
||||||
|
success: false,
|
||||||
|
status: 0,
|
||||||
|
responseTime: 0,
|
||||||
|
body: null
|
||||||
|
};
|
||||||
|
|
||||||
|
try {
|
||||||
|
const payload = {
|
||||||
|
targetUrl: targetUrl,
|
||||||
|
method: config.httpMethod,
|
||||||
|
headers: config.customHeaders,
|
||||||
|
body: config.requestBody
|
||||||
|
};
|
||||||
|
|
||||||
|
const payloadStr = JSON.stringify(payload);
|
||||||
|
state.bytesSent += payloadStr.length;
|
||||||
|
|
||||||
|
const response = await fetch(config.proxyUrl, {
|
||||||
|
method: 'POST',
|
||||||
|
headers: { 'Content-Type': 'application/json' },
|
||||||
|
body: payloadStr
|
||||||
|
});
|
||||||
|
|
||||||
|
const proxyResponse = await response.json();
|
||||||
|
const endTime = performance.now();
|
||||||
|
|
||||||
|
result.responseTime = proxyResponse.responseTime || (endTime - startTime);
|
||||||
|
result.status = proxyResponse.statusCode;
|
||||||
|
result.success = proxyResponse.success && result.status >= 200 && result.status < 400;
|
||||||
|
result.body = proxyResponse.body;
|
||||||
|
|
||||||
|
if (result.body) {
|
||||||
|
state.bytesReceived += result.body.length;
|
||||||
|
}
|
||||||
|
|
||||||
|
updateStats(result);
|
||||||
|
|
||||||
|
} catch (error) {
|
||||||
|
result.responseTime = performance.now() - startTime;
|
||||||
|
state.failedRequests++;
|
||||||
|
state.errorsByCategory["network"]++;
|
||||||
|
}
|
||||||
|
|
||||||
|
return result;
|
||||||
|
}
|
||||||
|
|
||||||
|
function updateStats(result) {
|
||||||
|
state.totalRequests++;
|
||||||
|
if (result.success) {
|
||||||
|
state.successfulRequests++;
|
||||||
|
} else {
|
||||||
|
state.failedRequests++;
|
||||||
|
const category = categorizeError(result.status);
|
||||||
|
state.errorsByCategory[category]++;
|
||||||
|
}
|
||||||
|
state.responseTimes.push(result.responseTime);
|
||||||
|
|
||||||
|
// Keep response times capped in worker to save memory
|
||||||
|
if (state.responseTimes.length > 500) {
|
||||||
|
state.responseTimes.shift();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function categorizeError(status) {
|
||||||
|
if (status >= 400 && status < 500) return "4xx";
|
||||||
|
if (status >= 500) return "5xx";
|
||||||
|
return "network";
|
||||||
|
}
|
||||||
|
|
||||||
|
function reportResults() {
|
||||||
|
self.postMessage({
|
||||||
|
type: 'STATS',
|
||||||
|
data: {
|
||||||
|
totalRequests: state.totalRequests,
|
||||||
|
successfulRequests: state.successfulRequests,
|
||||||
|
failedRequests: state.failedRequests,
|
||||||
|
bytesSent: state.bytesSent,
|
||||||
|
bytesReceived: state.bytesReceived,
|
||||||
|
errorsByCategory: state.errorsByCategory,
|
||||||
|
responseTimes: state.responseTimes // Sampled
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
// Clear local counters that are cumulative but reported incrementally if needed
|
||||||
|
// Actually, state object above is cumulative. Main thread will track totals.
|
||||||
|
}
|
||||||
|
|
||||||
|
function extractRandomLink(html, baseUrl) {
|
||||||
|
try {
|
||||||
|
const linkRegex = /href=["'](https?:\/\/[^"']+|(?:\/[^"']+))["']/gi;
|
||||||
|
const links = [];
|
||||||
|
let match;
|
||||||
|
const baseUrlObj = new URL(baseUrl);
|
||||||
|
|
||||||
|
while ((match = linkRegex.exec(html)) !== null) {
|
||||||
|
let href = match[1];
|
||||||
|
try {
|
||||||
|
const absoluteUrl = new URL(href, baseUrl);
|
||||||
|
if (absoluteUrl.hostname === baseUrlObj.hostname) {
|
||||||
|
links.push(absoluteUrl.href);
|
||||||
|
}
|
||||||
|
} catch (e) { }
|
||||||
|
if (links.length > 50) break; // Limit extraction
|
||||||
|
}
|
||||||
|
|
||||||
|
if (links.length > 0) {
|
||||||
|
return links[Math.floor(Math.random() * links.length)];
|
||||||
|
}
|
||||||
|
} catch (e) { }
|
||||||
|
return null;
|
||||||
|
}
|
||||||
Reference in New Issue
Block a user