From 178c324bf1cc6c0d2c07dcb875c1c8772d0adfe3 Mon Sep 17 00:00:00 2001 From: 6C656C65 <73671374+6C656C65@users.noreply.github.com> Date: Sun, 8 Jun 2025 22:49:59 +0200 Subject: [PATCH 1/8] fix: client.py wrap recv with try/except --- pyproxy/handlers/client.py | 8 +++++++- 1 file changed, 7 insertions(+), 1 deletion(-) diff --git a/pyproxy/handlers/client.py b/pyproxy/handlers/client.py index bb3bac7..da33d62 100644 --- a/pyproxy/handlers/client.py +++ b/pyproxy/handlers/client.py @@ -90,7 +90,13 @@ def handle_client(self, client_socket): Args: client_socket (socket): The socket object for the client connection. """ - request = client_socket.recv(4096) + try: + request = client_socket.recv(4096) + except ConnectionResetError: + self.console_logger.debug("Connection reset by peer during recv, closing socket.") + client_socket.close() + self.active_connections.pop(threading.get_ident(), None) + return if not request: self.console_logger.debug("No request received, closing connection.") From 288b23718122439449340ed465c0e03fde85e0da Mon Sep 17 00:00:00 2001 From: 6C656C65 <73671374+6C656C65@users.noreply.github.com> Date: Sun, 8 Jun 2025 23:16:05 +0200 Subject: [PATCH 2/8] fix: self.active_connections update dict --- pyproxy/handlers/client.py | 42 +++++++++++++++++--------------------- pyproxy/handlers/http.py | 4 ++-- pyproxy/handlers/https.py | 4 ++-- pyproxy/server.py | 1 + 4 files changed, 24 insertions(+), 27 deletions(-) diff --git a/pyproxy/handlers/client.py b/pyproxy/handlers/client.py index da33d62..4f621e8 100644 --- a/pyproxy/handlers/client.py +++ b/pyproxy/handlers/client.py @@ -91,29 +91,25 @@ def handle_client(self, client_socket): client_socket (socket): The socket object for the client connection. """ try: + client_socket.settimeout(10) request = client_socket.recv(4096) - except ConnectionResetError: - self.console_logger.debug("Connection reset by peer during recv, closing socket.") - client_socket.close() - self.active_connections.pop(threading.get_ident(), None) - return - - if not request: - self.console_logger.debug("No request received, closing connection.") - client_socket.close() - self.active_connections.pop(threading.get_ident(), None) - return - first_line = request.decode(errors="ignore").split("\n")[0] + if not request: + return - if first_line.startswith("CONNECT"): - https_handler = self._create_handler( - HttpsHandler, - ssl_config=self.ssl_config, - cancel_inspect_queue=self.cancel_inspect_queue, - cancel_inspect_result_queue=self.cancel_inspect_result_queue, - ) - https_handler.handle_https_connection(client_socket, first_line) - else: - http_handler = self._create_handler(HttpHandler) - http_handler.handle_http_request(client_socket, request) + first_line = request.decode(errors="ignore").split("\n")[0] + print("debug server.py :", threading.get_ident()) + if first_line.startswith("CONNECT"): + https_handler = self._create_handler( + HttpsHandler, + ssl_config=self.ssl_config, + cancel_inspect_queue=self.cancel_inspect_queue, + cancel_inspect_result_queue=self.cancel_inspect_result_queue, + ) + https_handler.handle_https_connection(client_socket, first_line) + else: + http_handler = self._create_handler(HttpHandler) + http_handler.handle_http_request(client_socket, request) + finally: + client_socket.close() + self.active_connections.pop(threading.get_ident(), None) \ No newline at end of file diff --git a/pyproxy/handlers/http.py b/pyproxy/handlers/http.py index 9451f43..33bef10 100644 --- a/pyproxy/handlers/http.py +++ b/pyproxy/handlers/http.py @@ -205,12 +205,12 @@ def forward_request_to_server(self, client_socket, request, url, first_line): thread_id = threading.get_ident() if thread_id in self.active_connections: - self.active_connections[thread_id] = { + self.active_connections[thread_id].update({ "target_ip": server_host, "target_port": server_port, "bytes_sent": 0, "bytes_received": 0, - } + }) try: server_socket = socket.socket(socket.AF_INET, socket.SOCK_STREAM) diff --git a/pyproxy/handlers/https.py b/pyproxy/handlers/https.py index e2b3deb..aaffd19 100644 --- a/pyproxy/handlers/https.py +++ b/pyproxy/handlers/https.py @@ -231,10 +231,10 @@ def handle_https_connection(self, client_socket, first_line): not_inspect = self._should_skip_inspection(server_host) thread_id = threading.get_ident() - self.active_connections[thread_id] = { + self.active_connections[thread_id].update({ "bytes_sent": 0, "bytes_received": 0, - } + }) if self.ssl_config.ssl_inspect and not not_inspect: try: diff --git a/pyproxy/server.py b/pyproxy/server.py index d3360e7..a10dcc7 100644 --- a/pyproxy/server.py +++ b/pyproxy/server.py @@ -362,5 +362,6 @@ def start(self): "bytes_received": 0, "thread_name": client_handler.name, } + print("debug server.py :", client_handler.ident) except KeyboardInterrupt: self.console_logger.info("Proxy interrupted, shutting down.") From be1c1da5b164020448f74bcebdab2681b4fe838f Mon Sep 17 00:00:00 2001 From: 6C656C65 <73671374+6C656C65@users.noreply.github.com> Date: Sun, 8 Jun 2025 23:29:40 +0200 Subject: [PATCH 3/8] remove print & monitoring: add target_ip & target_domain --- pyproxy/handlers/client.py | 1 - pyproxy/handlers/http.py | 9 ++++++++- pyproxy/handlers/https.py | 1 + pyproxy/monitoring/templates/index.html | 2 +- pyproxy/server.py | 1 - 5 files changed, 10 insertions(+), 4 deletions(-) diff --git a/pyproxy/handlers/client.py b/pyproxy/handlers/client.py index 4f621e8..9ea816d 100644 --- a/pyproxy/handlers/client.py +++ b/pyproxy/handlers/client.py @@ -98,7 +98,6 @@ def handle_client(self, client_socket): return first_line = request.decode(errors="ignore").split("\n")[0] - print("debug server.py :", threading.get_ident()) if first_line.startswith("CONNECT"): https_handler = self._create_handler( HttpsHandler, diff --git a/pyproxy/handlers/http.py b/pyproxy/handlers/http.py index 33bef10..a6cb97b 100644 --- a/pyproxy/handlers/http.py +++ b/pyproxy/handlers/http.py @@ -202,11 +202,18 @@ def forward_request_to_server(self, client_socket, request, url, first_line): server_port = parsed_url.port or ( 443 if parsed_url.scheme == "https" else 80 ) + + try: + ip_address = socket.gethostbyname(server_host) + except socket.gaierror: + ip_address = server_host + thread_id = threading.get_ident() if thread_id in self.active_connections: self.active_connections[thread_id].update({ - "target_ip": server_host, + "target_ip": ip_address, + "target_domain": server_host, "target_port": server_port, "bytes_sent": 0, "bytes_received": 0, diff --git a/pyproxy/handlers/https.py b/pyproxy/handlers/https.py index aaffd19..eaafdca 100644 --- a/pyproxy/handlers/https.py +++ b/pyproxy/handlers/https.py @@ -232,6 +232,7 @@ def handle_https_connection(self, client_socket, first_line): thread_id = threading.get_ident() self.active_connections[thread_id].update({ + "target_domain": server_host, "bytes_sent": 0, "bytes_received": 0, }) diff --git a/pyproxy/monitoring/templates/index.html b/pyproxy/monitoring/templates/index.html index 7cc65ae..bbe9175 100644 --- a/pyproxy/monitoring/templates/index.html +++ b/pyproxy/monitoring/templates/index.html @@ -58,7 +58,7 @@

Active Connections

: data.active_connections.map(conn => `

Client: ${conn.client_ip}:${conn.client_port}

-

Target: ${conn.target_ip}:${conn.target_port}

+

Target: ${conn.target_domain} (${conn.target_ip}:${conn.target_port})

Sent: ${conn.bytes_sent} bytes

Received: ${formatBytes(conn.bytes_received)}

diff --git a/pyproxy/server.py b/pyproxy/server.py index a10dcc7..d3360e7 100644 --- a/pyproxy/server.py +++ b/pyproxy/server.py @@ -362,6 +362,5 @@ def start(self): "bytes_received": 0, "thread_name": client_handler.name, } - print("debug server.py :", client_handler.ident) except KeyboardInterrupt: self.console_logger.info("Proxy interrupted, shutting down.") From 0f3af14436d6c7e787bd64ffd6c23650457210b2 Mon Sep 17 00:00:00 2001 From: 6C656C65 <73671374+6C656C65@users.noreply.github.com> Date: Mon, 9 Jun 2025 00:06:23 +0200 Subject: [PATCH 4/8] monitoring: extract js from html --- pyproxy/handlers/https.py | 20 +++++- pyproxy/monitoring/static/monitoring.js | 84 ++++++++++++++++++++++++ pyproxy/monitoring/templates/index.html | 87 +------------------------ 3 files changed, 103 insertions(+), 88 deletions(-) create mode 100644 pyproxy/monitoring/static/monitoring.js diff --git a/pyproxy/handlers/https.py b/pyproxy/handlers/https.py index eaafdca..f3a5ea1 100644 --- a/pyproxy/handlers/https.py +++ b/pyproxy/handlers/https.py @@ -206,6 +206,21 @@ def _process_first_ssl_request(self, ssl_client_socket, server_host, first_line) if self._is_blocked(f"{server_host}{path}"): return None, full_url, True + if not self.logger_config.no_logging_access: + method, domain_port, protocol = first_line.split(" ") + domain, port = domain_port.split(":") + self.logger_config.access_logger.info( + "", + extra={ + "ip_src": ssl_client_socket.getpeername()[0], + "url": full_url, + "method": method, + "domain": server_host, + "port": port, + "protocol": protocol, + }, + ) + return first_request, full_url, False except Exception as e: self.logger_config.error_logger.error(f"SSL request processing error : {e}") @@ -318,13 +333,13 @@ def handle_https_connection(self, client_socket, first_line): ) if not self.logger_config.no_logging_access: - _, _, protocol = first_line.split(" ") + method, _, protocol = first_line.split(" ") self.logger_config.access_logger.info( "", extra={ "ip_src": client_ip, "url": target, - "method": "CONNECT", + "method": method, "domain": server_host, "port": server_port, "protocol": protocol, @@ -398,6 +413,7 @@ def transfer_data_between_sockets(self, client_socket, server_socket): data ) except (socket.error, OSError): + self.logger_config.console_logger("error") client_socket.close() server_socket.close() self.active_connections.pop(threading.get_ident(), None) diff --git a/pyproxy/monitoring/static/monitoring.js b/pyproxy/monitoring/static/monitoring.js new file mode 100644 index 0000000..cebf2de --- /dev/null +++ b/pyproxy/monitoring/static/monitoring.js @@ -0,0 +1,84 @@ +async function fetchMonitoringData() { + try { + const response = await fetch('/monitoring'); + const data = await response.json(); + + document.getElementById('status-section').innerHTML = ` +

Main Process

+

Name: ${data.name}

+

PID: ${data.pid}

+

Status: ${data.status}

+

Start Time: ${data.start_time}

+ `; + + document.getElementById('subprocesses-section').innerHTML = ` +

Subprocesses

+ ${Object.values(data.subprocesses).map(proc => ` +
+

${proc.name}

+

PID: ${proc.pid}

+

Status: ${proc.status}

+ +
+ `).join('')} + `; + + document.getElementById('connections-section').innerHTML = ` +

Active Connections

+ ${data.active_connections.length === 0 + ? '

No active connections.

' + : data.active_connections.map(conn => ` +
+

Client: ${conn.client_ip}:${conn.client_port}

+

Target: ${conn.target_domain} (${conn.target_ip}:${conn.target_port})

+

Sent: ${conn.bytes_sent} bytes

+

Received: ${formatBytes(conn.bytes_received)}

+
+ `).join('')} + `; + } catch (err) { + console.error('Error loading data:', err); + } +} + +async function fetchConfigData() { + try { + const response = await fetch('/config'); + const config = await response.json(); + + document.getElementById('config-section').innerHTML = ` +

Configuration ${config.debug ? 'DEBUG' : ''}

+

Port: ${config.port ? `${config.port}` : ''}

+

Flask Port: ${config.flask_port ? `${config.flask_port}` : ''}

+

HTML 403: ${config.html_403 ? `${config.html_403}` : ''}

+

Filter Configuration ${config.filter_config.no_filter ? '✗' : '✓'}

+

Blocked Sites File: ${config.filter_config.blocked_sites ? `${config.filter_config.blocked_sites}` : ''}

+

Blocked URL File: ${config.filter_config.blocked_url ? `${config.filter_config.blocked_url}` : ''}

+

Filter Mode: ${config.filter_config.filter_mode}

+

Logger Configuration

+

Access Log: ${config.logger_config.no_logging_access ? '' : ''} ${config.logger_config.access_log ? `${config.logger_config.access_log}` : ''}

+

Block Log: ${config.logger_config.no_logging_block ? '' : ''} ${config.logger_config.block_log ? `${config.logger_config.block_log}` : ''}

+

SSL Inspection ${config.ssl_config.ssl_inspect ? '✓' : '✗'}

+

Inspect CA Cert: ${config.ssl_config.inspect_ca_cert ? `${config.ssl_config.inspect_ca_cert}` : ''}

+

Inspect CA Key: ${config.ssl_config.inspect_ca_key ? `${config.ssl_config.inspect_ca_key}` : ''}

+

Inspect certs folder: ${config.ssl_config.inspect_certs_folder ? `${config.ssl_config.inspect_certs_folder}` : ''}

+

Cancel inspect: ${config.ssl_config.cancel_inspect ? `${config.ssl_config.cancel_inspect}` : ''}

+ `; + } catch (err) { + console.error('Error loading config data:', err); + } +} + +function formatBytes(bytes) { + if (bytes === 0) return '0 B'; + const sizes = ['B', 'KB', 'MB', 'GB', 'TB']; + const i = Math.floor(Math.log(bytes) / Math.log(1024)); + const value = bytes / Math.pow(1024, i); + return value.toFixed(2) + ' ' + sizes[i]; +} + +// Initial fetches and intervals +fetchMonitoringData(); +fetchConfigData(); +setInterval(fetchMonitoringData, 5000); +setInterval(fetchConfigData, 5000); diff --git a/pyproxy/monitoring/templates/index.html b/pyproxy/monitoring/templates/index.html index bbe9175..12d6b1b 100644 --- a/pyproxy/monitoring/templates/index.html +++ b/pyproxy/monitoring/templates/index.html @@ -24,91 +24,6 @@

- - + From ecb89c6c2576686f4faf07d9c24fcbbeeb675191 Mon Sep 17 00:00:00 2001 From: 6C656C65 <73671374+6C656C65@users.noreply.github.com> Date: Mon, 9 Jun 2025 00:22:55 +0200 Subject: [PATCH 5/8] monitoring: refresh same tab --- pyproxy/monitoring/static/monitoring.js | 50 ++++++++++++++++++++++++- pyproxy/monitoring/static/style.css | 32 ++++++++++++++++ pyproxy/monitoring/templates/index.html | 39 ++++++++++++------- 3 files changed, 106 insertions(+), 15 deletions(-) diff --git a/pyproxy/monitoring/static/monitoring.js b/pyproxy/monitoring/static/monitoring.js index cebf2de..0b1ab00 100644 --- a/pyproxy/monitoring/static/monitoring.js +++ b/pyproxy/monitoring/static/monitoring.js @@ -77,7 +77,55 @@ function formatBytes(bytes) { return value.toFixed(2) + ' ' + sizes[i]; } -// Initial fetches and intervals +const tabs = document.querySelectorAll('.tab'); +const contents = document.querySelectorAll('.tab-content'); + +function activateTab(tab) { + tabs.forEach(t => { + t.classList.remove('active'); + t.setAttribute('aria-selected', 'false'); + t.setAttribute('tabindex', '-1'); + }); + contents.forEach(c => c.classList.remove('active')); + + tab.classList.add('active'); + tab.setAttribute('aria-selected', 'true'); + tab.setAttribute('tabindex', '0'); + const contentId = tab.getAttribute('aria-controls'); + document.getElementById(contentId).classList.add('active'); + + localStorage.setItem('activeTabId', tab.id); +} + +tabs.forEach(tab => { + tab.addEventListener('click', () => { + activateTab(tab); + }); + + tab.addEventListener('keydown', e => { + let index = Array.from(tabs).indexOf(e.target); + if (e.key === 'ArrowRight') { + index = (index + 1) % tabs.length; + tabs[index].focus(); + } else if (e.key === 'ArrowLeft') { + index = (index - 1 + tabs.length) % tabs.length; + tabs[index].focus(); + } + }); +}); + +window.addEventListener('DOMContentLoaded', () => { + const savedTabId = localStorage.getItem('activeTabId'); + if (savedTabId) { + const savedTab = document.getElementById(savedTabId); + if (savedTab) { + activateTab(savedTab); + return; + } + } + activateTab(tabs[0]); +}); + fetchMonitoringData(); fetchConfigData(); setInterval(fetchMonitoringData, 5000); diff --git a/pyproxy/monitoring/static/style.css b/pyproxy/monitoring/static/style.css index 0c8bae3..7941bb7 100644 --- a/pyproxy/monitoring/static/style.css +++ b/pyproxy/monitoring/static/style.css @@ -109,3 +109,35 @@ h1 { vertical-align: middle; font-style: italic; } + +.tabs { + display: flex; + gap: 20px; + margin-bottom: 25px; + cursor: pointer; + user-select: none; + border-bottom: 2px solid #ddd; +} + +.tab { + padding: 10px 20px; + border-radius: 6px 6px 0 0; + background: #eee; + color: #444; + font-weight: bold; + transition: background 0.3s; +} +.tab.active { + background: white; + border: 2px solid #ddd; + border-bottom: none; + color: #0066cc; +} + +.tab-content { + display: none; +} + +.tab-content.active { + display: block; +} \ No newline at end of file diff --git a/pyproxy/monitoring/templates/index.html b/pyproxy/monitoring/templates/index.html index 12d6b1b..ea25424 100644 --- a/pyproxy/monitoring/templates/index.html +++ b/pyproxy/monitoring/templates/index.html @@ -1,29 +1,40 @@ - - Proxy Monitoring - - + + PyProxy Monitoring + +

- icon + icon PyProxy Monitoring

-
-
-
Loading...
-
-
-
-
-
Loading config...
-
+ +
+ + + +
+ + +
+
Loading...
+
+
+ +
+
Loading config...
+
+ +
+
+ From 9ff3c417f8aa18bcf716c64dc66ae7a86d51ac1f Mon Sep 17 00:00:00 2001 From: 6C656C65 <73671374+6C656C65@users.noreply.github.com> Date: Mon, 9 Jun 2025 18:01:52 +0200 Subject: [PATCH 6/8] monitoring: add refresh timer --- pyproxy/monitoring/static/monitoring.js | 72 ++++++++++++++----------- pyproxy/monitoring/templates/index.html | 4 +- 2 files changed, 43 insertions(+), 33 deletions(-) diff --git a/pyproxy/monitoring/static/monitoring.js b/pyproxy/monitoring/static/monitoring.js index 0b1ab00..21f6e98 100644 --- a/pyproxy/monitoring/static/monitoring.js +++ b/pyproxy/monitoring/static/monitoring.js @@ -1,19 +1,26 @@ -async function fetchMonitoringData() { +let countdown = 2; + +async function fetchAllData() { try { - const response = await fetch('/monitoring'); - const data = await response.json(); + const [monitoringRes, configRes] = await Promise.all([ + fetch('/monitoring'), + fetch('/config') + ]); + + const monitoring = await monitoringRes.json(); + const config = await configRes.json(); document.getElementById('status-section').innerHTML = `

Main Process

-

Name: ${data.name}

-

PID: ${data.pid}

-

Status: ${data.status}

-

Start Time: ${data.start_time}

+

Name: ${monitoring.name}

+

PID: ${monitoring.pid}

+

Status: ${monitoring.status}

+

Start Time: ${monitoring.start_time}

`; document.getElementById('subprocesses-section').innerHTML = `

Subprocesses

- ${Object.values(data.subprocesses).map(proc => ` + ${Object.values(monitoring.subprocesses).map(proc => `

${proc.name}

PID: ${proc.pid}

@@ -25,9 +32,9 @@ async function fetchMonitoringData() { document.getElementById('connections-section').innerHTML = `

Active Connections

- ${data.active_connections.length === 0 + ${monitoring.active_connections.length === 0 ? '

No active connections.

' - : data.active_connections.map(conn => ` + : monitoring.active_connections.map(conn => `

Client: ${conn.client_ip}:${conn.client_port}

Target: ${conn.target_domain} (${conn.target_ip}:${conn.target_port})

@@ -36,15 +43,6 @@ async function fetchMonitoringData() {
`).join('')} `; - } catch (err) { - console.error('Error loading data:', err); - } -} - -async function fetchConfigData() { - try { - const response = await fetch('/config'); - const config = await response.json(); document.getElementById('config-section').innerHTML = `

Configuration ${config.debug ? 'DEBUG' : ''}

@@ -64,19 +62,36 @@ async function fetchConfigData() {

Inspect certs folder: ${config.ssl_config.inspect_certs_folder ? `${config.ssl_config.inspect_certs_folder}` : ''}

Cancel inspect: ${config.ssl_config.cancel_inspect ? `${config.ssl_config.cancel_inspect}` : ''}

`; + } catch (err) { - console.error('Error loading config data:', err); + console.error('Error loading data:', err); } + countdown = 2; } function formatBytes(bytes) { if (bytes === 0) return '0 B'; const sizes = ['B', 'KB', 'MB', 'GB', 'TB']; const i = Math.floor(Math.log(bytes) / Math.log(1024)); - const value = bytes / Math.pow(1024, i); - return value.toFixed(2) + ' ' + sizes[i]; + return (bytes / Math.pow(1024, i)).toFixed(2) + ' ' + sizes[i]; +} + +function updateCountdown() { + document.getElementById('refresh-timer').textContent = formatCountdown(countdown); +} + +function formatCountdown(seconds) { + const m = String(Math.floor(seconds / 60)).padStart(2, '0'); + const s = String(seconds % 60).padStart(2, '0'); + return `${m}:${s}`; } +setInterval(() => { + countdown--; + if (countdown <= 0) fetchAllData(); + updateCountdown(); +}, 1000); + const tabs = document.querySelectorAll('.tab'); const contents = document.querySelectorAll('.tab-content'); @@ -118,15 +133,10 @@ window.addEventListener('DOMContentLoaded', () => { const savedTabId = localStorage.getItem('activeTabId'); if (savedTabId) { const savedTab = document.getElementById(savedTabId); - if (savedTab) { - activateTab(savedTab); - return; - } + if (savedTab) activateTab(savedTab); } activateTab(tabs[0]); -}); -fetchMonitoringData(); -fetchConfigData(); -setInterval(fetchMonitoringData, 5000); -setInterval(fetchConfigData, 5000); + fetchAllData(); + updateCountdown(); +}); diff --git a/pyproxy/monitoring/templates/index.html b/pyproxy/monitoring/templates/index.html index ea25424..812a100 100644 --- a/pyproxy/monitoring/templates/index.html +++ b/pyproxy/monitoring/templates/index.html @@ -5,6 +5,7 @@ PyProxy Monitoring +
@@ -12,6 +13,7 @@

icon PyProxy Monitoring

+

Next refresh in: --:--

@@ -34,7 +36,5 @@

- - From 937ed0d3caeb3b04c046e55fdb34f8343eea4502 Mon Sep 17 00:00:00 2001 From: 6C656C65 <73671374+6C656C65@users.noreply.github.com> Date: Mon, 9 Jun 2025 18:41:30 +0200 Subject: [PATCH 7/8] monitoring: split web.py --- pyproxy/monitoring/__init__.py | 38 +++++ pyproxy/monitoring/auth.py | 31 ++++ pyproxy/monitoring/monitor.py | 196 +++++++++++++++++++++++ pyproxy/monitoring/routes.py | 74 +++++++++ pyproxy/monitoring/web.py | 279 --------------------------------- pyproxy/server.py | 2 +- 6 files changed, 340 insertions(+), 280 deletions(-) create mode 100644 pyproxy/monitoring/auth.py create mode 100644 pyproxy/monitoring/monitor.py create mode 100644 pyproxy/monitoring/routes.py delete mode 100644 pyproxy/monitoring/web.py diff --git a/pyproxy/monitoring/__init__.py b/pyproxy/monitoring/__init__.py index e69de29..4111fb3 100644 --- a/pyproxy/monitoring/__init__.py +++ b/pyproxy/monitoring/__init__.py @@ -0,0 +1,38 @@ +""" +pyproxy.monitoring.__init__.py + +Provides a monitoring system for the ProxyServer, exposing information about +processes, threads, active connections, and subprocesses. Implements an HTTP +server using Flask to provide monitoring endpoints. +""" + +import logging +from flask import Flask +from .monitor import ProxyMonitor +from .auth import create_basic_auth +from .routes import register_routes + + +def start_flask_server(proxy_server, flask_port, flask_pass, debug) -> None: + """ + Launches a Flask HTTP server to monitor the ProxyServer. + + The server exposes endpoints that provide status information about + the proxy server, including process details, thread information, + subprocess statuses, and active connections. + + Args: + proxy_server (ProxyServer): The ProxyServer instance to monitor. + flask_port (int): The port number on which the Flask server will listen. + flask_pass (str): The password used for basic HTTP authentication. + debug (bool): Flag to enable or disable Flask debug mode. + """ + auth = create_basic_auth(flask_pass) + + app = Flask(__name__, static_folder="static") + if not debug: + log = logging.getLogger("werkzeug") + log.setLevel(logging.ERROR) + + register_routes(app, auth, proxy_server, ProxyMonitor) + app.run(host="0.0.0.0", port=flask_port) # nosec diff --git a/pyproxy/monitoring/auth.py b/pyproxy/monitoring/auth.py new file mode 100644 index 0000000..5efede1 --- /dev/null +++ b/pyproxy/monitoring/auth.py @@ -0,0 +1,31 @@ +""" +pyproxy.monitoring.auth.py + +Provides HTTP Basic Authentication setup for the monitoring Flask server, +using a single hardcoded user 'admin' with a hashed password. +""" + +from flask_httpauth import HTTPBasicAuth +from werkzeug.security import check_password_hash, generate_password_hash + + +def create_basic_auth(password: str): + """ + Creates and configures an HTTPBasicAuth instance with a single user. + + Args: + password (str): The password for the 'admin' user. + + Returns: + HTTPBasicAuth: Configured HTTPBasicAuth instance for use in Flask. + """ + auth = HTTPBasicAuth() + users = {"admin": generate_password_hash(password)} + + @auth.verify_password + def verify_password(username, passwd): + if username in users and check_password_hash(users.get(username), passwd): + return username + return None + + return auth diff --git a/pyproxy/monitoring/monitor.py b/pyproxy/monitoring/monitor.py new file mode 100644 index 0000000..e99029a --- /dev/null +++ b/pyproxy/monitoring/monitor.py @@ -0,0 +1,196 @@ +""" +pyproxy.monitoring.monitor + +This module defines the ProxyMonitor class, which provides monitoring capabilities +for the ProxyServer. It collects and exposes real-time information about the +main process, threads, subprocesses, and active client connections using `psutil`, +`threading`, and `multiprocessing` libraries. +""" + +import os +import threading +import multiprocessing +from datetime import datetime +from typing import List, Dict, Union +import psutil + + +class ProxyMonitor: + """ + Monitors the status of the ProxyServer, including details about + the main process, threads, subprocesses, and active client connections. + + Args: + proxy_server (ProxyServer): The instance of the ProxyServer to monitor. + """ + + def __init__(self, proxy_server): + self.proxy_server = proxy_server + + def get_process_info( + self, + ) -> Dict[str, Union[int, str, List[Dict[str, Union[int, str]]]]]: + """ + Retrieves overall process information for the ProxyServer, + including the PID, name, status, and details about threads, + subprocesses, and active connections. + + Returns: + dict: A dictionary containing the process information. + """ + process_info = { + "pid": os.getpid(), + "name": "ProxyServer", + "status": "running", + "start_time": datetime.fromtimestamp( + psutil.Process(os.getpid()).create_time() + ).strftime("%Y-%m-%d %H:%M:%S"), + "threads": self.get_threads_info(), + "subprocesses": self.get_subprocesses_info(), + "active_connections": self.get_active_connections(), + } + return process_info + + def get_threads_info(self) -> List[Dict[str, Union[int, str]]]: + """ + Retrieves information about the threads running in the ProxyServer. + + Returns: + list: A list of dictionaries, each containing information + about a thread. + """ + threads_info = [] + for thread in threading.enumerate(): + threads_info.append( + { + "thread_id": thread.ident, + "name": thread.name, + "status": self.get_thread_status(thread), + } + ) + return threads_info + + def get_thread_status(self, thread: threading.Thread) -> str: + """ + Gets the status of a given thread. + + Args: + thread (threading.Thread): The thread whose status is to be retrieved. + + Returns: + str: The status of the thread ('running', 'terminated', or 'unknown'). + """ + try: + if thread.is_alive(): + return "running" + return "terminated" + except AttributeError: + return "unknown" + + def get_subprocesses_info( + self, + ) -> Dict[str, Dict[str, Union[str, List[Dict[str, Union[int, str]]]]]]: + """ + Retrieves the status of the ProxyServer's subprocesses, including + filtering, shortcuts, cancel inspection, and custom header processes. + + Returns: + dict: A dictionary containing subprocess statuses. + """ + subprocesses_info = {} + + subprocesses = { + "filter": self.proxy_server.filter_proc, + "shortcuts": self.proxy_server.shortcuts_proc, + "cancel_inspect": self.proxy_server.cancel_inspect_proc, + "custom_header": self.proxy_server.custom_header_proc, + } + + for name, process in subprocesses.items(): + if process is not None and process.is_alive(): + subprocesses_info[name] = self.get_subprocess_status(process, name) + return subprocesses_info + + def get_subprocess_status( + self, process: multiprocessing.Process, name: str + ) -> Dict[str, Union[str, None, List[Dict[str, Union[int, str]]]]]: + """ + Retrieves the status of a subprocess. + + Args: + process (multiprocessing.Process): The subprocess to check. + name (str): The name of the subprocess. + + Returns: + dict: A dictionary containing the subprocess status. + """ + if process is None: + return {"status": "not started", "name": name, "threads": []} + try: + status = "running" if process.is_alive() else "terminated" + threads_info = self.get_subprocess_threads_info(process) + except AttributeError: + status = "terminated" + threads_info = [] + return { + "pid": process.pid if hasattr(process, "pid") else None, + "status": status, + "name": name, + "threads": threads_info, + } + + def get_subprocess_threads_info( + self, process: multiprocessing.Process + ) -> List[Dict[str, Union[int, str]]]: + """ + Retrieves the threads associated with a subprocess. + + Args: + process (multiprocessing.Process): The subprocess to check. + + Returns: + list: A list of dictionaries containing thread information. + """ + threads_info = [] + try: + for proc_thread in psutil.Process(process.pid).threads(): + threads_info.append( + { + "thread_id": proc_thread.id, + "name": f"Thread-{proc_thread.id}", + "status": self.get_thread_status_by_pid(proc_thread.id), + } + ) + except (psutil.NoSuchProcess, psutil.AccessDenied): + pass + return threads_info + + def get_thread_status_by_pid(self, thread_id: int) -> str: + """ + Attempts to retrieve the status of a thread by its PID. + + Args: + thread_id (int): The thread's ID. + + Returns: + str: The status of the thread ('running' or 'terminated'). + """ + try: + process = psutil.Process(thread_id) + if process.is_running(): + return "running" + return "terminated" + except psutil.NoSuchProcess: + return "terminated" + + def get_active_connections(self) -> List[Dict[str, Union[int, Dict]]]: + """ + Retrieves information about the active client connections to the ProxyServer. + + Returns: + list: A list of dictionaries containing information about active connections. + """ + return [ + {"thread_id": thread_id, **conn} + for thread_id, conn in self.proxy_server.active_connections.items() + ] \ No newline at end of file diff --git a/pyproxy/monitoring/routes.py b/pyproxy/monitoring/routes.py new file mode 100644 index 0000000..ad700fa --- /dev/null +++ b/pyproxy/monitoring/routes.py @@ -0,0 +1,74 @@ +""" +pyproxy.monitoring.routes.py + +Defines and registers monitoring-related routes for the Flask application, +including endpoints for system information, configuration, and a secured +HTML-based index page. +""" + +from flask import jsonify, render_template + + +def register_routes(app, auth, proxy_server, ProxyMonitor): + """ + Registers the monitoring routes to the Flask app, secured with HTTP Basic Auth. + + Args: + app (Flask): The Flask application instance. + auth (HTTPBasicAuth): The HTTP Basic Auth instance used to secure routes. + proxy_server (ProxyServer): The running ProxyServer instance to monitor. + ProxyMonitor (class): The monitoring class used to gather runtime information. + """ + + @app.route("/") + @auth.login_required + def index(): + """ + Serves the main index HTML page for the monitoring dashboard. + + Returns: + Response: Rendered HTML page. + """ + return render_template("index.html") + + @app.route("/monitoring", methods=["GET"]) + @auth.login_required + def monitoring(): + """ + Provides real-time monitoring information about the ProxyServer, + including process, thread, and connection status. + + Returns: + Response: JSON object containing monitoring data. + """ + monitor = ProxyMonitor(proxy_server) + return jsonify(monitor.get_process_info()) + + @app.route("/config", methods=["GET"]) + @auth.login_required + def config(): + """ + Returns the current configuration of the ProxyServer, including + host, port, debug mode, and optional components like logger, filter, + and SSL configuration. + + Returns: + Response: JSON object containing configuration data. + """ + config_data = { + "host": proxy_server.host_port[0], + "port": proxy_server.host_port[1], + "debug": proxy_server.debug, + "html_403": proxy_server.html_403, + "logger_config": ( + proxy_server.logger_config.to_dict() if proxy_server.logger_config else None + ), + "filter_config": ( + proxy_server.filter_config.to_dict() if proxy_server.filter_config else None + ), + "ssl_config": ( + proxy_server.ssl_config.to_dict() if proxy_server.ssl_config else None + ), + "flask_port": proxy_server.monitoring_config.flask_port, + } + return jsonify(config_data) \ No newline at end of file diff --git a/pyproxy/monitoring/web.py b/pyproxy/monitoring/web.py deleted file mode 100644 index 9d6e105..0000000 --- a/pyproxy/monitoring/web.py +++ /dev/null @@ -1,279 +0,0 @@ -""" -pyproxy.monitoring.web.py - -This module defines a monitoring system for the ProxyServer that provides -information about the server's processes, threads, active connections, and -subprocesses. It includes an HTTP server implemented with Flask to expose -monitoring endpoints for the proxy server. -""" - -import os -import threading -import multiprocessing -import logging -from datetime import datetime -from typing import List, Dict, Union -from flask import Flask, jsonify, render_template -from flask_httpauth import HTTPBasicAuth -from werkzeug.security import check_password_hash, generate_password_hash -import psutil - - -def start_flask_server(proxy_server, flask_port, flask_pass, debug) -> None: - """ - Starts the Flask server for monitoring the ProxyServer. It creates and - runs an HTTP server that exposes the proxy server's status, including - process and thread details, subprocess statuses, and active connections. - - Args: - proxy_server (ProxyServer): The ProxyServer instance to monitor. - - The server will expose two routes: - - '/' : Renders a simple index page. - - '/monitoring' : Returns a JSON response with the monitoring data. - """ - - class ProxyMonitor: - """ - Monitors the status of the ProxyServer, including process, thread, - and subprocess information, as well as active client connections. - - Args: - proxy_server (ProxyServer): The ProxyServer instance to monitor. - """ - - def __init__(self, proxy_server): - self.proxy_server = proxy_server - - def get_process_info( - self, - ) -> Dict[str, Union[int, str, List[Dict[str, Union[int, str]]]]]: - """ - Retrieves overall process information for the ProxyServer, - including the PID, name, status, and details about threads, - subprocesses, and active connections. - - Returns: - dict: A dictionary containing the process information. - """ - process_info = { - "pid": os.getpid(), - "name": "ProxyServer", - "status": "running", - "start_time": datetime.fromtimestamp( - psutil.Process(os.getpid()).create_time() - ).strftime("%Y-%m-%d %H:%M:%S"), - "threads": self.get_threads_info(), - "subprocesses": self.get_subprocesses_info(), - "active_connections": self.get_active_connections(), - } - return process_info - - def get_threads_info(self) -> List[Dict[str, Union[int, str]]]: - """ - Retrieves information about the threads running in the ProxyServer. - - Returns: - list: A list of dictionaries, each containing information - about a thread. - """ - threads_info = [] - for thread in threading.enumerate(): - threads_info.append( - { - "thread_id": thread.ident, - "name": thread.name, - "status": self.get_thread_status(thread), - } - ) - return threads_info - - def get_thread_status(self, thread: threading.Thread) -> str: - """ - Gets the status of a given thread. - - Args: - thread (threading.Thread): The thread whose status is to be retrieved. - - Returns: - str: The status of the thread ('running', 'terminated', or 'unknown'). - """ - try: - if thread.is_alive(): - return "running" - return "terminated" - except AttributeError: - return "unknown" - - def get_subprocesses_info( - self, - ) -> Dict[str, Dict[str, Union[str, List[Dict[str, Union[int, str]]]]]]: - """ - Retrieves the status of the ProxyServer's subprocesses, including - filtering, shortcuts, cancel inspection, and custom header processes. - - Returns: - dict: A dictionary containing subprocess statuses. - """ - subprocesses_info = {} - - subprocesses = { - "filter": self.proxy_server.filter_proc, - "shortcuts": self.proxy_server.shortcuts_proc, - "cancel_inspect": self.proxy_server.cancel_inspect_proc, - "custom_header": self.proxy_server.custom_header_proc, - } - - for name, process in subprocesses.items(): - if process is not None and process.is_alive(): - subprocesses_info[name] = self.get_subprocess_status(process, name) - return subprocesses_info - - def get_subprocess_status( - self, process: multiprocessing.Process, name: str - ) -> Dict[str, Union[str, None, List[Dict[str, Union[int, str]]]]]: - """ - Retrieves the status of a subprocess. - - Args: - process (multiprocessing.Process): The subprocess to check. - name (str): The name of the subprocess. - - Returns: - dict: A dictionary containing the subprocess status. - """ - if process is None: - return {"status": "not started", "name": name, "threads": []} - try: - status = "running" if process.is_alive() else "terminated" - threads_info = self.get_subprocess_threads_info(process) - except AttributeError: - status = "terminated" - threads_info = [] - return { - "pid": process.pid if hasattr(process, "pid") else None, - "status": status, - "name": name, - "threads": threads_info, - } - - def get_subprocess_threads_info( - self, process: multiprocessing.Process - ) -> List[Dict[str, Union[int, str]]]: - """ - Retrieves the threads associated with a subprocess. - - Args: - process (multiprocessing.Process): The subprocess to check. - - Returns: - list: A list of dictionaries containing thread information. - """ - threads_info = [] - try: - for proc_thread in psutil.Process(process.pid).threads(): - threads_info.append( - { - "thread_id": proc_thread.id, - "name": f"Thread-{proc_thread.id}", - "status": self.get_thread_status_by_pid(proc_thread.id), - } - ) - except (psutil.NoSuchProcess, psutil.AccessDenied): - pass - return threads_info - - def get_thread_status_by_pid(self, thread_id: int) -> str: - """ - Attempts to retrieve the status of a thread by its PID. - - Args: - thread_id (int): The thread's ID. - - Returns: - str: The status of the thread ('running' or 'terminated'). - """ - try: - process = psutil.Process(thread_id) - if process.is_running(): - return "running" - return "terminated" - except psutil.NoSuchProcess: - return "terminated" - - def get_active_connections(self) -> List[Dict[str, Union[int, Dict]]]: - """ - Retrieves information about the active client connections to the ProxyServer. - - Returns: - list: A list of dictionaries containing information about active connections. - """ - return [ - {"thread_id": thread_id, **conn} - for thread_id, conn in self.proxy_server.active_connections.items() - ] - - auth = HTTPBasicAuth() - - users = {"admin": generate_password_hash(flask_pass)} - - @auth.verify_password - def verify_password(username, password): - if username in users and check_password_hash(users.get(username), password): - return username - return None - - app = Flask(__name__, static_folder="static") - if not debug: - log = logging.getLogger("werkzeug") - log.setLevel(logging.ERROR) - - @app.route("/") - @auth.login_required - def index(): - """ - Renders the index page for the Flask application. - - Returns: - str: The rendered HTML content of the index page. - """ - return render_template("index.html") - - @app.route("/monitoring", methods=["GET"]) - @auth.login_required - def monitoring(): - """ - Returns the monitoring data for the ProxyServer in JSON format. - - Returns: - Response: A JSON response containing the server's process information. - """ - monitor = ProxyMonitor(proxy_server) - return jsonify(monitor.get_process_info()) - - @app.route("/config", methods=["GET"]) - @auth.login_required - def config(): - config_data = { - "host": proxy_server.host_port[0], - "port": proxy_server.host_port[1], - "debug": proxy_server.debug, - "html_403": proxy_server.html_403, - "logger_config": ( - proxy_server.logger_config.to_dict() - if proxy_server.logger_config - else None - ), - "filter_config": ( - proxy_server.filter_config.to_dict() - if proxy_server.filter_config - else None - ), - "ssl_config": ( - proxy_server.ssl_config.to_dict() if proxy_server.ssl_config else None - ), - "flask_port": proxy_server.monitoring_config.flask_port, - } - return jsonify(config_data) - - app.run(host="0.0.0.0", port=flask_port) # nosec diff --git a/pyproxy/server.py b/pyproxy/server.py index d3360e7..68c79e9 100644 --- a/pyproxy/server.py +++ b/pyproxy/server.py @@ -27,7 +27,7 @@ if not __slim__: from pyproxy.modules.custom_header import custom_header_process if not __slim__: - from pyproxy.monitoring.web import start_flask_server + from pyproxy.monitoring import start_flask_server class ProxyServer: From 4843dfe5b7bae466129ec7c2036100d17912aab5 Mon Sep 17 00:00:00 2001 From: 6C656C65 <73671374+6C656C65@users.noreply.github.com> Date: Mon, 9 Jun 2025 18:50:34 +0200 Subject: [PATCH 8/8] fix black --- pyproxy/handlers/client.py | 2 +- pyproxy/handlers/http.py | 16 +++++++++------- pyproxy/handlers/https.py | 12 +++++++----- pyproxy/monitoring/monitor.py | 2 +- pyproxy/monitoring/routes.py | 10 +++++++--- 5 files changed, 25 insertions(+), 17 deletions(-) diff --git a/pyproxy/handlers/client.py b/pyproxy/handlers/client.py index 9ea816d..2612b65 100644 --- a/pyproxy/handlers/client.py +++ b/pyproxy/handlers/client.py @@ -111,4 +111,4 @@ def handle_client(self, client_socket): http_handler.handle_http_request(client_socket, request) finally: client_socket.close() - self.active_connections.pop(threading.get_ident(), None) \ No newline at end of file + self.active_connections.pop(threading.get_ident(), None) diff --git a/pyproxy/handlers/http.py b/pyproxy/handlers/http.py index a6cb97b..7ab193d 100644 --- a/pyproxy/handlers/http.py +++ b/pyproxy/handlers/http.py @@ -211,13 +211,15 @@ def forward_request_to_server(self, client_socket, request, url, first_line): thread_id = threading.get_ident() if thread_id in self.active_connections: - self.active_connections[thread_id].update({ - "target_ip": ip_address, - "target_domain": server_host, - "target_port": server_port, - "bytes_sent": 0, - "bytes_received": 0, - }) + self.active_connections[thread_id].update( + { + "target_ip": ip_address, + "target_domain": server_host, + "target_port": server_port, + "bytes_sent": 0, + "bytes_received": 0, + } + ) try: server_socket = socket.socket(socket.AF_INET, socket.SOCK_STREAM) diff --git a/pyproxy/handlers/https.py b/pyproxy/handlers/https.py index f3a5ea1..17ec5ce 100644 --- a/pyproxy/handlers/https.py +++ b/pyproxy/handlers/https.py @@ -246,11 +246,13 @@ def handle_https_connection(self, client_socket, first_line): not_inspect = self._should_skip_inspection(server_host) thread_id = threading.get_ident() - self.active_connections[thread_id].update({ - "target_domain": server_host, - "bytes_sent": 0, - "bytes_received": 0, - }) + self.active_connections[thread_id].update( + { + "target_domain": server_host, + "bytes_sent": 0, + "bytes_received": 0, + } + ) if self.ssl_config.ssl_inspect and not not_inspect: try: diff --git a/pyproxy/monitoring/monitor.py b/pyproxy/monitoring/monitor.py index e99029a..0a33423 100644 --- a/pyproxy/monitoring/monitor.py +++ b/pyproxy/monitoring/monitor.py @@ -193,4 +193,4 @@ def get_active_connections(self) -> List[Dict[str, Union[int, Dict]]]: return [ {"thread_id": thread_id, **conn} for thread_id, conn in self.proxy_server.active_connections.items() - ] \ No newline at end of file + ] diff --git a/pyproxy/monitoring/routes.py b/pyproxy/monitoring/routes.py index ad700fa..d5aeb6c 100644 --- a/pyproxy/monitoring/routes.py +++ b/pyproxy/monitoring/routes.py @@ -61,14 +61,18 @@ def config(): "debug": proxy_server.debug, "html_403": proxy_server.html_403, "logger_config": ( - proxy_server.logger_config.to_dict() if proxy_server.logger_config else None + proxy_server.logger_config.to_dict() + if proxy_server.logger_config + else None ), "filter_config": ( - proxy_server.filter_config.to_dict() if proxy_server.filter_config else None + proxy_server.filter_config.to_dict() + if proxy_server.filter_config + else None ), "ssl_config": ( proxy_server.ssl_config.to_dict() if proxy_server.ssl_config else None ), "flask_port": proxy_server.monitoring_config.flask_port, } - return jsonify(config_data) \ No newline at end of file + return jsonify(config_data)