diff --git a/.bumpversion.cfg b/.bumpversion.cfg index 21a9f94..5dd36ca 100644 --- a/.bumpversion.cfg +++ b/.bumpversion.cfg @@ -1,5 +1,5 @@ [bumpversion] -current_version = 0.4.5 +current_version = 0.4.6 commit = True tag = True diff --git a/CHANGELOG.md b/CHANGELOG.md index f76a3f7..8cc6ed8 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,3 +1,21 @@ +## [0.4.6] - 2025-06-12 +### Added +- Monitoring : Actions connections table +- Monitoring : Add & Delete blocked domains/URLs +- Monitoring : Searchbar (filerting & Actives connections) +### Change +- Monitoring : change routes + +## [0.4.5] - 2025-06-09 +### Added +- Monitoring : refresh timer +### Change +- Rework monitoring code + +## [0.4.4] - 2025-06-08 +### Change +- Repository migration + ## [0.4.3] - 2025-06-08 ### Added - Docs: installation from compose diff --git a/pyproxy/__init__.py b/pyproxy/__init__.py index 4e8acf1..d043ce1 100644 --- a/pyproxy/__init__.py +++ b/pyproxy/__init__.py @@ -5,7 +5,7 @@ import os -__version__ = "0.4.5" +__version__ = "0.4.6" if os.path.isdir("pyproxy/monitoring"): __slim__ = False diff --git a/pyproxy/handlers/https.py b/pyproxy/handlers/https.py index 17ec5ce..08be289 100644 --- a/pyproxy/handlers/https.py +++ b/pyproxy/handlers/https.py @@ -415,7 +415,6 @@ 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/routes.py b/pyproxy/monitoring/routes.py index d5aeb6c..c49bb45 100644 --- a/pyproxy/monitoring/routes.py +++ b/pyproxy/monitoring/routes.py @@ -6,7 +6,7 @@ HTML-based index page. """ -from flask import jsonify, render_template +from flask import jsonify, render_template, request def register_routes(app, auth, proxy_server, ProxyMonitor): @@ -31,7 +31,7 @@ def index(): """ return render_template("index.html") - @app.route("/monitoring", methods=["GET"]) + @app.route("/api/status", methods=["GET"]) @auth.login_required def monitoring(): """ @@ -44,13 +44,20 @@ def monitoring(): monitor = ProxyMonitor(proxy_server) return jsonify(monitor.get_process_info()) - @app.route("/config", methods=["GET"]) + @app.route("/api/settings", 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 the current configuration of the ProxyServer. + + The configuration includes: + - Host and port + - Debug mode + - 403 HTML page usage + - Logger configuration (if present) + - Filter configuration (if present) + - SSL configuration (if present) + - Flask monitoring port Returns: Response: JSON object containing configuration data. @@ -76,3 +83,129 @@ def config(): "flask_port": proxy_server.monitoring_config.flask_port, } return jsonify(config_data) + + @app.route("/api/filtering", methods=["GET", "POST", "DELETE"]) + @auth.login_required + def blocked(): + """ + Manages the blocked sites and URLs list. + + GET: + Reads and returns the current blocked domains and URLs from the corresponding files. + Returns: + Response: JSON object with 'blocked_sites' and 'blocked_url' lists. + + POST: + Adds a new domain or URL to the blocked lists based on + 'type' and 'value' from JSON input. + Request JSON: + { + "type": "domain" | "url", + "value": "" + } + Returns: + 201: Successfully added. + 400: Invalid input. + 409: Value already blocked. + + DELETE: + Removes a domain or URL from the blocked lists based on + 'type' and 'value' from JSON input. + Request JSON: + { + "type": "domain" | "url", + "value": "" + } + Returns: + 200: Successfully removed. + 400: Invalid input. + 404: Value not found. + 500: Server error. + """ + if request.method == "GET": + blocked_sites_content = "" + blocked_url_content = "" + + with open( + proxy_server.filter_config.blocked_sites, "r", encoding="utf-8" + ) as f: + blocked_sites_content = [line.strip() for line in f if line.strip()] + with open( + proxy_server.filter_config.blocked_url, "r", encoding="utf-8" + ) as f: + blocked_url_content = [line.strip() for line in f if line.strip()] + + blocked_data = { + "blocked_sites": blocked_sites_content, + "blocked_url": blocked_url_content, + } + return jsonify(blocked_data) + + elif request.method == "POST": + data = request.get_json() + typ = data.get("type") + val = data.get("value", "").strip() + if not val or typ not in ["domain", "url"]: + return jsonify({"error": "Invalid input"}), 400 + + filename = ( + proxy_server.filter_config.blocked_sites + if typ == "domain" + else proxy_server.filter_config.blocked_url + ) + + with open(filename, "r+", encoding="utf-8") as f: + lines = [line.strip() for line in f if line.strip()] + if val in lines: + return jsonify({"message": "Already blocked"}), 409 + lines.append(val) + f.seek(0) + f.truncate() + f.write("\n".join(lines) + "\n") + return jsonify({"message": "Added successfully"}), 201 + + elif request.method == "DELETE": + data = request.get_json() + if not data or "type" not in data or "value" not in data: + return ( + jsonify({"error": "Missing 'type' or 'value' in request body"}), + 400, + ) + + block_type = data["type"] + value = data["value"].strip() + + if block_type == "domain": + filepath = proxy_server.filter_config.blocked_sites + elif block_type == "url": + filepath = proxy_server.filter_config.blocked_url + else: + return ( + jsonify({"error": "Invalid type, must be 'domain' or 'url'"}), + 400, + ) + + try: + with open(filepath, "r", encoding="utf-8") as f: + lines = [line.strip() for line in f if line.strip()] + + if value not in lines: + return ( + jsonify({"error": f"{value} not found in {block_type} list"}), + 404, + ) + + lines = [line for line in lines if line != value] + + with open(filepath, "w", encoding="utf-8") as f: + for line in lines: + f.write(line + "\n") + + return ( + jsonify( + {"message": f"{block_type} '{value}' removed successfully"} + ), + 200, + ) + except Exception as e: + return jsonify({"error": f"Server error: {str(e)}"}), 500 diff --git a/pyproxy/monitoring/static/monitoring.js b/pyproxy/monitoring/static/monitoring.js index 21f6e98..304a55c 100644 --- a/pyproxy/monitoring/static/monitoring.js +++ b/pyproxy/monitoring/static/monitoring.js @@ -2,13 +2,15 @@ let countdown = 2; async function fetchAllData() { try { - const [monitoringRes, configRes] = await Promise.all([ - fetch('/monitoring'), - fetch('/config') + const [monitoringRes, configRes, blockedRes] = await Promise.all([ + fetch('/api/status'), + fetch('/api/settings'), + fetch('/api/filtering') ]); const monitoring = await monitoringRes.json(); const config = await configRes.json(); + const blocked = await blockedRes.json(); document.getElementById('status-section').innerHTML = `

Main Process

@@ -30,18 +32,31 @@ async function fetchAllData() { `).join('')} `; - document.getElementById('connections-section').innerHTML = ` -

Active Connections

+ document.getElementById('connections-table-container').innerHTML = ` ${monitoring.active_connections.length === 0 ? '

No active connections.

' - : monitoring.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('')} + : ` + + + + + + + + + + + ${monitoring.active_connections.map(conn => ` + + + + + + + `).join('')} + +
ClientTargetSentReceived
${conn.client_ip}:${conn.client_port}${conn.target_domain} (${conn.target_ip}:${conn.target_port})${conn.bytes_sent} bytes${formatBytes(conn.bytes_received)}
+ `} `; document.getElementById('config-section').innerHTML = ` @@ -63,6 +78,75 @@ async function fetchAllData() {

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

`; + const searchInput = document.getElementById('connection-search'); + if (searchInput) { + filterConnections(searchInput.value); + } + + const blockedSites = blocked.blocked_sites || []; + const blockedUrls = blocked.blocked_url || []; + + const blockedSection = document.getElementById('blocked-section-container'); + if (blockedSection) { + blockedSection.innerHTML = ` +
+

Blocked sites

+ ${blockedSites.length === 0 + ? '

No blocked sites.

' + : ` + + + + + + + + + ${blockedSites.map(site => ` + + + + + `).join('')} + +
DomainAction
${site} + +
+ `} +
+
+

Blocked URLs

+ ${blockedUrls.length === 0 + ? '

No URLs blocked.

' + : ` + + + + + + + + + ${blockedUrls.map(url => ` + + + + + `).join('')} + +
URLAction
${url} + +
+ `} +
+ `; + } + + const blockedSearchInput = document.getElementById('blocked-search'); + if (blockedSearchInput) { + filterBlocked(blockedSearchInput.value); + } + } catch (err) { console.error('Error loading data:', err); } @@ -76,6 +160,30 @@ function formatBytes(bytes) { return (bytes / Math.pow(1024, i)).toFixed(2) + ' ' + sizes[i]; } +function handleUnblock(type, value) { + fetch('/api/filtering', { + method: 'DELETE', + headers: { + 'Content-Type': 'application/json', + }, + body: JSON.stringify({ + type: type, + value: value + }), + }) + .then(response => { + if (!response.ok) { + alert(`Error while deleting : ${value}`); + } else { + fetchAllData(); + } + }) + .catch(err => { + console.error('Fetching error:', err); + alert('Network error'); + }); +} + function updateCountdown() { document.getElementById('refresh-timer').textContent = formatCountdown(countdown); } @@ -86,6 +194,55 @@ function formatCountdown(seconds) { return `${m}:${s}`; } +function filterConnections(filter) { + filter = filter.toLowerCase(); + const rows = document.querySelectorAll('#connections-table-container tbody tr'); + rows.forEach(row => { + const client = row.children[0].textContent.toLowerCase(); + const target = row.children[1].textContent.toLowerCase(); + + row.querySelectorAll('td').forEach(td => { + td.innerHTML = td.textContent; + }); + + const match = client.includes(filter) || target.includes(filter); + row.style.display = match ? '' : 'none'; + + if (match && filter.length > 0) { + if (client.includes(filter)) { + const originalText = row.children[0].textContent; + const regex = new RegExp(`(${filter})`, 'gi'); + row.children[0].innerHTML = originalText.replace(regex, '$1'); + } + if (target.includes(filter)) { + const originalText = row.children[1].textContent; + const regex = new RegExp(`(${filter})`, 'gi'); + row.children[1].innerHTML = originalText.replace(regex, '$1'); + } + } + }); +} + +function filterBlocked(filter) { + filter = filter.toLowerCase(); + + document.querySelectorAll('.blocked-subsection table').forEach(table => { + table.querySelectorAll('tbody tr').forEach(row => { + const text = row.children[0].textContent.toLowerCase(); + const match = text.includes(filter); + row.style.display = match ? '' : 'none'; + + row.children[0].innerHTML = row.children[0].textContent; + + if (match && filter.length > 0) { + const originalText = row.children[0].textContent; + const regex = new RegExp(`(${filter})`, 'gi'); + row.children[0].innerHTML = originalText.replace(regex, '$1'); + } + }); + }); +} + setInterval(() => { countdown--; if (countdown <= 0) fetchAllData(); @@ -133,9 +290,63 @@ window.addEventListener('DOMContentLoaded', () => { const savedTabId = localStorage.getItem('activeTabId'); if (savedTabId) { const savedTab = document.getElementById(savedTabId); - if (savedTab) activateTab(savedTab); + if (savedTab) { + activateTab(savedTab); + } else { + activateTab(tabs[0]); + } + } else { + activateTab(tabs[0]); + } + + const searchInput = document.getElementById('connection-search'); + if (searchInput) { + searchInput.addEventListener('input', () => { + filterConnections(searchInput.value); + }); } - activateTab(tabs[0]); + + const blockedSearchInput = document.getElementById('blocked-search'); + if (blockedSearchInput) { + blockedSearchInput.addEventListener('input', () => { + filterBlocked(blockedSearchInput.value); + }); + } + + document.getElementById('add-block-form').addEventListener('submit', function(event) { + event.preventDefault(); + + const type = document.getElementById('block-type').value; + const value = document.getElementById('block-value').value.trim(); + + if (!value) { + alert('Please enter a value to block.'); + return; + } + + fetch('/api/filtering', { + method: 'POST', + headers: { + 'Content-Type': 'application/json', + }, + body: JSON.stringify({ + type: type, + value: value + }), + }) + .then(response => { + if (response.ok) { + document.getElementById('block-value').value = ''; + fetchAllData(); + } else { + alert(`Error adding : ${value}`); + } + }) + .catch(err => { + console.error('Network error:', err); + alert('Network error'); + }); + }); fetchAllData(); updateCountdown(); diff --git a/pyproxy/monitoring/static/style.css b/pyproxy/monitoring/static/style.css index 7941bb7..74de6bb 100644 --- a/pyproxy/monitoring/static/style.css +++ b/pyproxy/monitoring/static/style.css @@ -140,4 +140,44 @@ h1 { .tab-content.active { display: block; +} + +.generic-table { + width: 100%; + border-collapse: collapse; + margin-top: 10px; +} + +.generic-table th, +.generic-table td { + border: 1px solid #ccc; + padding: 8px; + text-align: left; +} + +.generic-table th { + background-color: #f2f2f2; +} + +.filtering-table th:first-child, +.filtering-table td:first-child { + width: 70%; +} + +.filtering-table th:last-child, +.filtering-table td:last-child { + width: 30%; + text-align: center; +} + +#connections-section { + max-width: 100%; +} + +input#connection-search, .connection-table { + box-sizing: border-box; +} + +.highlight { + background-color: yellow; } \ No newline at end of file diff --git a/pyproxy/monitoring/templates/index.html b/pyproxy/monitoring/templates/index.html index 812a100..f1da872 100644 --- a/pyproxy/monitoring/templates/index.html +++ b/pyproxy/monitoring/templates/index.html @@ -20,6 +20,7 @@

+ @@ -29,11 +30,38 @@

-
Loading config...
+
Loading...
-
+
+

Active Connections

+ +
+
+
+ +