mirror of
https://github.com/Av0idD4rk/firegex.git
synced 2026-10-23 16:31:00 +00:00
356 lines
13 KiB
Python
Executable File
356 lines
13 KiB
Python
Executable File
#!/usr/bin/env python3
|
|
"""
|
|
Firegex HttpHistory Real-Time Stress Test.
|
|
Connects to Firegex API, deploys a service carrying a pyfilter that uses HttpHistory,
|
|
starts an HTTP 200 OK backend server and continuous HTTP client requests.
|
|
Measures real-time memory usage (Firegex cpproxy / local process) and request throughput until stopped (Ctrl+C).
|
|
"""
|
|
|
|
import sys
|
|
import os
|
|
import time
|
|
import signal
|
|
import socket
|
|
import threading
|
|
import argparse
|
|
from typing import Optional, List
|
|
|
|
# Add project paths
|
|
sys.path.insert(0, os.path.abspath(os.path.join(os.path.dirname(__file__), "..")))
|
|
sys.path.insert(0, os.path.abspath(os.path.join(os.path.dirname(__file__), "../..")))
|
|
sys.path.insert(0, os.path.abspath(os.path.join(os.path.dirname(__file__), "../../fgex-lib")))
|
|
|
|
try:
|
|
import psutil
|
|
HAS_PSUTIL = True
|
|
except ImportError:
|
|
HAS_PSUTIL = False
|
|
|
|
from tests.utils.colors import colors, puts, sep
|
|
from tests.utils.firegexapi import FiregexAPI
|
|
|
|
HTTP_STRESS_FILTER_CODE = """
|
|
from firegex.pyfilters.models import HttpFullRequest, HttpFullResponse, HttpHistory
|
|
from firegex.pyfilters import pyfilter, ACCEPT
|
|
|
|
req_count = 0
|
|
resp_count = 0
|
|
|
|
@pyfilter
|
|
def history_stress_req(req: HttpFullRequest, history: HttpHistory):
|
|
global req_count
|
|
req_count += 1
|
|
_ = len(history.requests)
|
|
_ = len(history.responses)
|
|
return ACCEPT
|
|
|
|
@pyfilter
|
|
def history_stress_resp(resp: HttpFullResponse, history: HttpHistory):
|
|
global resp_count
|
|
resp_count += 1
|
|
_ = len(history.requests)
|
|
_ = len(history.responses)
|
|
return ACCEPT
|
|
"""
|
|
|
|
|
|
def start_http_backend_server(port: int, ipv6: bool = False):
|
|
"""Starts a multithreaded HTTP backend server that returns valid HTTP 200 OK responses."""
|
|
server_sock = socket.socket(socket.AF_INET6 if ipv6 else socket.AF_INET, socket.SOCK_STREAM)
|
|
server_sock.setsockopt(socket.SOL_SOCKET, socket.SO_REUSEADDR, 1)
|
|
server_sock.bind(("::1" if ipv6 else "127.0.0.1", port))
|
|
server_sock.listen(128)
|
|
server_sock.settimeout(0.5)
|
|
|
|
running = True
|
|
|
|
def handle_client(conn):
|
|
conn.settimeout(2.0)
|
|
resp = b"HTTP/1.1 200 OK\r\nContent-Type: text/plain\r\nContent-Length: 12\r\nConnection: keep-alive\r\n\r\nHello World!"
|
|
try:
|
|
while running:
|
|
buf = conn.recv(4096)
|
|
if not buf:
|
|
break
|
|
conn.sendall(resp)
|
|
except Exception:
|
|
pass
|
|
finally:
|
|
try:
|
|
conn.close()
|
|
except Exception:
|
|
pass
|
|
|
|
def server_loop():
|
|
while running:
|
|
try:
|
|
conn, _ = server_sock.accept()
|
|
t = threading.Thread(target=handle_client, args=(conn,), daemon=True)
|
|
t.start()
|
|
except socket.timeout:
|
|
continue
|
|
except Exception:
|
|
break
|
|
try:
|
|
server_sock.close()
|
|
except Exception:
|
|
pass
|
|
|
|
t = threading.Thread(target=server_loop, daemon=True)
|
|
t.start()
|
|
|
|
def stop_server():
|
|
nonlocal running
|
|
running = False
|
|
try:
|
|
server_sock.close()
|
|
except Exception:
|
|
pass
|
|
|
|
return stop_server
|
|
|
|
|
|
def find_cpproxy_process():
|
|
"""Finds cpproxy process using psutil if running."""
|
|
if not HAS_PSUTIL:
|
|
return None
|
|
try:
|
|
for proc in psutil.process_iter(['pid', 'name', 'cmdline']):
|
|
name = proc.info['name'] or ''
|
|
cmdline = ' '.join(proc.info['cmdline'] or [])
|
|
if 'cpproxy' in name or 'cpproxy' in cmdline:
|
|
return proc
|
|
except Exception:
|
|
pass
|
|
return None
|
|
|
|
|
|
def get_memory_mb(proc=None) -> float:
|
|
"""Returns memory usage in MB for a given process or current process."""
|
|
try:
|
|
if proc:
|
|
return proc.memory_info().rss / (1024 * 1024)
|
|
if HAS_PSUTIL:
|
|
return psutil.Process(os.getpid()).memory_info().rss / (1024 * 1024)
|
|
except Exception:
|
|
pass
|
|
return 0.0
|
|
|
|
|
|
def create_client_socket(port: int, ipv6: bool = False) -> socket.socket:
|
|
s = socket.socket(socket.AF_INET6 if ipv6 else socket.AF_INET, socket.SOCK_STREAM)
|
|
s.settimeout(2.0)
|
|
s.connect(("::1" if ipv6 else "127.0.0.1", port))
|
|
return s
|
|
|
|
|
|
def main():
|
|
parser = argparse.ArgumentParser(description="Firegex HttpHistory Stress Test")
|
|
parser.add_argument("--address", "-a", type=str, default="http://127.0.0.1:4444/", help="Firegex API address (default: http://127.0.0.1:4444/)")
|
|
parser.add_argument("--password", "-p", type=str, required=True, help="Firegex admin password")
|
|
parser.add_argument("--service-name", "-n", type=str, default="StressTestHistory", help="Service name (default: StressTestHistory)")
|
|
parser.add_argument("--port", "-P", type=int, default=1337, help="Service port (default: 1337)")
|
|
parser.add_argument("--ipv6", "-6", action="store_true", default=False, help="Use IPv6")
|
|
parser.add_argument("--transport", "-t", type=str, default="nfqueue",
|
|
choices=["nfqueue", "proxy"],
|
|
help="Network layer to run the filter on (default: nfqueue)")
|
|
parser.add_argument("--connections", "-c", type=int, default=4, help="Number of concurrent client connections (default: 4)")
|
|
args = parser.parse_args()
|
|
|
|
sep()
|
|
puts("🔥 Firegex HttpHistory Real-Time Stress Test 🔥", color=colors.cyan, is_bold=True)
|
|
puts(f"Firegex API: {args.address}", color=colors.yellow)
|
|
puts(f"Target Port: {args.port}", color=colors.yellow)
|
|
puts(f"Connections: {args.connections}", color=colors.yellow)
|
|
puts("Press Ctrl+C at any time to stop the stress test.", color=colors.magenta, is_bold=True)
|
|
sep()
|
|
|
|
firegex = FiregexAPI(args.address)
|
|
|
|
# 1. Login to Firegex
|
|
puts("Logging in to Firegex API...", color=colors.cyan)
|
|
if firegex.login(args.password):
|
|
puts("Successfully logged in ✔", color=colors.green)
|
|
else:
|
|
puts("Error: Login failed! Check password or address ✗", color=colors.red)
|
|
sys.exit(1)
|
|
|
|
# 2. Cleanup existing service if any
|
|
for srv in firegex.services_list():
|
|
if srv.get("name") == args.service_name or any(
|
|
a.get("port") == args.port for a in srv.get("addresses", [])
|
|
):
|
|
puts(f"Cleaning up existing service ID {srv['service_id']}...", color=colors.yellow)
|
|
firegex.services_delete(srv["service_id"])
|
|
|
|
# 3. Create service in Firegex
|
|
ip_int = "::1" if args.ipv6 else "127.0.0.1"
|
|
service_id = firegex.services_add(
|
|
args.service_name, ip_int, args.port, args.transport, fail_open=True)
|
|
if not service_id:
|
|
puts("Error: Failed to create the service in Firegex ✗", color=colors.red)
|
|
sys.exit(1)
|
|
puts(f"Created service ID: {service_id} ✔", color=colors.green)
|
|
|
|
def cleanup_and_exit(code=0):
|
|
puts("\nCleaning up Firegex service...", color=colors.yellow)
|
|
try:
|
|
firegex.services_stop(service_id)
|
|
firegex.services_delete(service_id)
|
|
puts("Successfully cleaned up Firegex service ✔", color=colors.green)
|
|
except Exception as e:
|
|
puts(f"Error during cleanup: {e}", color=colors.red)
|
|
sys.exit(code)
|
|
|
|
# 4. Upload the filter. No protocol is passed: the file asks for HttpHistory, and
|
|
# that is what makes it an HTTP filter.
|
|
puts("Deploying HttpHistory pyfilter code to Firegex...", color=colors.cyan)
|
|
if not firegex.services_add_filter(service_id, "pyfilter", "history"):
|
|
puts("Error: Failed to attach the pyfilter ✗", color=colors.red)
|
|
cleanup_and_exit(1)
|
|
filter_id = firegex.services_filters(service_id)[0]["filter_id"]
|
|
if not firegex.services_set_code(service_id, filter_id, HTTP_STRESS_FILTER_CODE):
|
|
puts("Error: Failed to upload pyfilter code ✗", color=colors.red)
|
|
cleanup_and_exit(1)
|
|
puts("Successfully deployed HttpHistory filter ✔", color=colors.green)
|
|
|
|
# 5. Start Firegex Service
|
|
puts("Starting Firegex service...", color=colors.cyan)
|
|
if not firegex.services_start(service_id):
|
|
puts("Error: Failed to start Firegex service ✗", color=colors.red)
|
|
cleanup_and_exit(1)
|
|
puts("Firegex service started successfully ✔", color=colors.green)
|
|
|
|
# 6. Start HTTP Backend Server
|
|
stop_backend_server = start_http_backend_server(args.port, ipv6=args.ipv6)
|
|
puts(f"HTTP Backend server listening on port {args.port} ✔", color=colors.green)
|
|
time.sleep(0.5)
|
|
|
|
# Find cpproxy process if available
|
|
cpproxy_proc = find_cpproxy_process()
|
|
if cpproxy_proc:
|
|
puts(f"Detected Firegex cpproxy process (PID {cpproxy_proc.pid}) ✔", color=colors.green)
|
|
|
|
# 7. Connect client sockets
|
|
running = True
|
|
|
|
def sig_handler(sig, frame):
|
|
nonlocal running
|
|
running = False
|
|
|
|
signal.signal(signal.SIGINT, sig_handler)
|
|
signal.signal(signal.SIGTERM, sig_handler)
|
|
|
|
client_socks: List[Optional[socket.socket]] = []
|
|
for _ in range(args.connections):
|
|
try:
|
|
client_socks.append(create_client_socket(args.port, args.ipv6))
|
|
except Exception:
|
|
client_socks.append(None)
|
|
|
|
puts(f"Initialized {len(client_socks)} client socket slot(s) ✔", color=colors.green)
|
|
puts("\n🚀 Spamming HTTP requests continuously. Press Ctrl+C to stop...\n", color=colors.cyan, is_bold=True)
|
|
|
|
req_index = 0
|
|
total_requests = 0
|
|
start_time = time.time()
|
|
last_print_time = start_time
|
|
last_req_count = 0
|
|
peak_script_mem = get_memory_mb()
|
|
peak_cpproxy_mem = get_memory_mb(cpproxy_proc) if cpproxy_proc else 0.0
|
|
|
|
try:
|
|
while running:
|
|
for i in range(len(client_socks)):
|
|
if not running:
|
|
break
|
|
|
|
s = client_socks[i]
|
|
if s is None:
|
|
try:
|
|
s = create_client_socket(args.port, args.ipv6)
|
|
client_socks[i] = s
|
|
except Exception:
|
|
continue
|
|
|
|
req_index += 1
|
|
req_bytes = f"GET /stress_req_{req_index} HTTP/1.1\r\nHost: 127.0.0.1\r\nConnection: keep-alive\r\nUser-Agent: FiregexStressClient\r\n\r\n".encode()
|
|
|
|
try:
|
|
s.sendall(req_bytes)
|
|
resp = s.recv(4096)
|
|
if resp and b"200 OK" in resp:
|
|
total_requests += 1
|
|
else:
|
|
raise ConnectionResetError("Invalid or empty HTTP response")
|
|
except Exception:
|
|
# Safely close broken socket and mark slot for reconnection
|
|
try:
|
|
s.close()
|
|
except Exception:
|
|
pass
|
|
client_socks[i] = None
|
|
|
|
curr_time = time.time()
|
|
elapsed_print = curr_time - last_print_time
|
|
|
|
if elapsed_print >= 1.0 or not running:
|
|
script_mem = get_memory_mb()
|
|
cpproxy_mem = get_memory_mb(cpproxy_proc) if cpproxy_proc else 0.0
|
|
|
|
if script_mem > peak_script_mem:
|
|
peak_script_mem = script_mem
|
|
if cpproxy_mem > peak_cpproxy_mem:
|
|
peak_cpproxy_mem = cpproxy_mem
|
|
|
|
batch_reqs = total_requests - last_req_count
|
|
reqs_per_sec = batch_reqs / elapsed_print if elapsed_print > 0 else 0
|
|
total_elapsed = curr_time - start_time
|
|
|
|
mem_str = f"Script RSS: {script_mem:.1f}MB"
|
|
if cpproxy_proc:
|
|
mem_str += f" | cpproxy RSS: {cpproxy_mem:.1f}MB (Peak: {peak_cpproxy_mem:.1f}MB)"
|
|
|
|
sys.stdout.write(
|
|
f"\r\033[K[{time.strftime('%H:%M:%S')}] Elapsed: {total_elapsed:.1f}s | "
|
|
f"Reqs: {total_requests:,} | "
|
|
f"Speed: {reqs_per_sec:,.0f} req/s | "
|
|
f"{mem_str}"
|
|
)
|
|
sys.stdout.flush()
|
|
|
|
last_print_time = curr_time
|
|
last_req_count = total_requests
|
|
|
|
except KeyboardInterrupt:
|
|
pass
|
|
|
|
end_time = time.time()
|
|
duration = end_time - start_time
|
|
|
|
# Close client sockets
|
|
for s in client_socks:
|
|
if s:
|
|
try:
|
|
s.close()
|
|
except Exception:
|
|
pass
|
|
|
|
stop_backend_server()
|
|
|
|
print() # New line
|
|
sep()
|
|
puts("🛑 Stress Test Interrupted by User", color=colors.yellow, is_bold=True)
|
|
puts(f"Total Time Elapsed: {duration:.2f} seconds")
|
|
puts(f"Total HTTP Reqs: {total_requests:,}")
|
|
puts(f"Average Throughput: {total_requests / max(duration, 0.001):,.0f} req/sec")
|
|
if cpproxy_proc:
|
|
puts(f"cpproxy Peak Memory: {peak_cpproxy_mem:.2f} MB", color=colors.cyan, is_bold=True)
|
|
sep()
|
|
|
|
cleanup_and_exit(0)
|
|
|
|
|
|
if __name__ == "__main__":
|
|
main()
|