From 39ac8e4756c9481b404af6ac82572038bcc4a01d Mon Sep 17 00:00:00 2001 From: 6C656C65 <73671374+6C656C65@users.noreply.github.com> Date: Fri, 31 Oct 2025 23:49:16 +0100 Subject: [PATCH 1/4] =?UTF-8?q?Bump=20version:=200.5.0=20=E2=86=92=200.6.0?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .bumpversion.cfg | 2 +- pyproxy/__init__.py | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/.bumpversion.cfg b/.bumpversion.cfg index 4bfbeba..7354c7e 100644 --- a/.bumpversion.cfg +++ b/.bumpversion.cfg @@ -1,5 +1,5 @@ [bumpversion] -current_version = 0.5.0 +current_version = 0.6.0 commit = True tag = True diff --git a/pyproxy/__init__.py b/pyproxy/__init__.py index 4288901..12a1ba9 100644 --- a/pyproxy/__init__.py +++ b/pyproxy/__init__.py @@ -5,7 +5,7 @@ import os -__version__ = "0.5.0" +__version__ = "0.6.0" if os.path.isdir("pyproxy/monitoring"): __slim__ = False From 6569d04bb84059293a232189233e58635a4b3fc3 Mon Sep 17 00:00:00 2001 From: 6C656C65 <73671374+6C656C65@users.noreply.github.com> Date: Sun, 14 Dec 2025 16:45:50 +0100 Subject: [PATCH 2/4] Consolidate configuration files into modular YAML format - Replace INI config with YAML format - Embed all config data (blocked sites, URLs, shortcuts, headers, IPs) directly in YAML - Remove separate .txt and .json config files - Update code to use embedded config data - Clean up unused imports and old code - Update tests for new config structure --- .dockerignore | 4 +- .gitignore | 4 -- config.ini.example | 44 ------------ config.yaml.example | 58 +++++++++++++++ config/authorized_ips.example.txt | 3 - config/blocked_sites.example.txt | 1 - config/blocked_url.example.txt | 1 - config/cancel_inspect.example.txt | 1 - config/custom_header.example.json | 9 --- config/shortcuts.example.txt | 3 - docker-compose.yml | 3 +- pyproxy/handlers/http.py | 7 +- pyproxy/modules/cancel_inspect.py | 38 ++-------- pyproxy/modules/custom_header.py | 55 ++------------- pyproxy/modules/filter.py | 99 +++++--------------------- pyproxy/modules/shortcuts.py | 34 +-------- pyproxy/pyproxy.py | 68 ++++++++---------- pyproxy/server.py | 25 ++----- pyproxy/utils/args.py | 27 +++---- pyproxy/utils/config.py | 12 ++-- requirements.txt | 1 + tests/modules/test_cancel_inspect.py | 17 +++-- tests/modules/test_custom_header.py | 11 ++- tests/modules/test_filter.py | 101 ++++++++++++--------------- tests/modules/test_shortcuts.py | 88 +++++++++-------------- 25 files changed, 241 insertions(+), 473 deletions(-) delete mode 100644 config.ini.example create mode 100644 config.yaml.example delete mode 100644 config/authorized_ips.example.txt delete mode 100644 config/blocked_sites.example.txt delete mode 100644 config/blocked_url.example.txt delete mode 100644 config/cancel_inspect.example.txt delete mode 100644 config/custom_header.example.json delete mode 100644 config/shortcuts.example.txt diff --git a/.dockerignore b/.dockerignore index 283864b..d7779eb 100644 --- a/.dockerignore +++ b/.dockerignore @@ -1,8 +1,6 @@ __pycache__/ */__pycache__/ -config/*.txt -config/*.json logs/*.log certs/*.key @@ -10,7 +8,7 @@ certs/*.pem !certs/ca/* tests/ -config.ini.example +config.yaml.example .github .bumpversion.cfg diff --git a/.gitignore b/.gitignore index eeca34e..90b3e0f 100644 --- a/.gitignore +++ b/.gitignore @@ -170,10 +170,6 @@ cython_debug/ # PyPI configuration file .pypirc -# Config file -config/* -!config/*.example* - # Certs folder certs/*.key certs/*.pem diff --git a/config.ini.example b/config.ini.example deleted file mode 100644 index aeedb10..0000000 --- a/config.ini.example +++ /dev/null @@ -1,44 +0,0 @@ -[Server] -host = 0.0.0.0 -port = 8080 - -[Logging] -debug = false -access_log = ./logs/access.log -block_log = ./logs/block.log -no_logging_access = false -no_logging_block = false -console_format = date=%(asctime)s level=%(levelname)s file=%(filename)s function=%(funcName)s message=%(message)s -access_log_format = date=%(asctime)s ip_src=%(ip_src)s url=%(url)s method=%(method)s domain=%(domain)s port=%(port)s protocol=%(protocol)s bytes_sent=%(bytes_sent)s bytes_received=%(bytes_received)s tls_version=%(tls_version)s -block_log_format = date=%(asctime)s ip_src=%(ip_src)s url=%(url)s method=%(method)s domain=%(domain)s port=%(port)s protocol=%(protocol)s -datefmt = %Y-%m-%d %H:%M:%S - -[Files] -html_403 = assets/403.html - -[Filtering] -no_filter = false -filter_mode = local -blocked_sites = config/blocked_sites.txt -blocked_url = config/blocked_url.txt - -[Options] -shortcuts = config/shortcuts.txt -custom_header = config/custom_header.json -authorized_ips = config/authorized_ips.txt - -[Security] -ssl_inspect = false -inspect_ca_cert = certs/ca/cert.pem -inspect_ca_key = certs/ca/key.pem -inspect_certs_folder = certs/ -cancel_inspect = config/cancel_inspect.txt - -[Monitoring] -flask_port = 5000 -flask_pass = password - -[Proxy] -proxy_enable = false -proxy_host = 127.0.0.1 -proxy_port = 8081 diff --git a/config.yaml.example b/config.yaml.example new file mode 100644 index 0000000..ea8c748 --- /dev/null +++ b/config.yaml.example @@ -0,0 +1,58 @@ +server: + host: 0.0.0.0 + port: 8080 + +logging: + debug: false + access_log: ./logs/access.log + block_log: ./logs/block.log + no_logging_access: false + no_logging_block: false + console_format: date=%(asctime)s level=%(levelname)s file=%(filename)s function=%(funcName)s message=%(message)s + access_log_format: date=%(asctime)s ip_src=%(ip_src)s url=%(url)s method=%(method)s domain=%(domain)s port=%(port)s protocol=%(protocol)s bytes_sent=%(bytes_sent)s bytes_received=%(bytes_received)s tls_version=%(tls_version)s + block_log_format: date=%(asctime)s ip_src=%(ip_src)s url=%(url)s method=%(method)s domain=%(domain)s port=%(port)s protocol=%(protocol)s + datefmt: '%Y-%m-%d %H:%M:%S' + +files: + html_403: assets/403.html + +filtering: + no_filter: false + filter_mode: local + blocked_sites: + - example.com + blocked_url: + - example2.com/about + +options: + shortcuts: + f: https://facebook.com + gh: https://github.com + yt: https://youtube.com + custom_header: + "https://example.com": + X-Custom-Header: HeaderValue + X-Another-Header: AnotherValue + "https://example2.com": + X-Different-Header: DifferentValue + authorized_ips: + - 0.0.0.0/0 + - 127.0.0.1/8 + - 10.0.0.1/32 + +security: + ssl_inspect: false + inspect_ca_cert: certs/ca/cert.pem + inspect_ca_key: certs/ca/key.pem + inspect_certs_folder: certs/ + cancel_inspect: + - mybank.com + +monitoring: + flask_port: 5000 + flask_pass: password + +proxy: + proxy_enable: false + proxy_host: 127.0.0.1 + proxy_port: 8081 \ No newline at end of file diff --git a/config/authorized_ips.example.txt b/config/authorized_ips.example.txt deleted file mode 100644 index c657d9f..0000000 --- a/config/authorized_ips.example.txt +++ /dev/null @@ -1,3 +0,0 @@ -0.0.0.0/0 -127.0.0.1/8 -10.0.0.1/32 \ No newline at end of file diff --git a/config/blocked_sites.example.txt b/config/blocked_sites.example.txt deleted file mode 100644 index caa12a8..0000000 --- a/config/blocked_sites.example.txt +++ /dev/null @@ -1 +0,0 @@ -example.com \ No newline at end of file diff --git a/config/blocked_url.example.txt b/config/blocked_url.example.txt deleted file mode 100644 index feffe98..0000000 --- a/config/blocked_url.example.txt +++ /dev/null @@ -1 +0,0 @@ -example2.com/about \ No newline at end of file diff --git a/config/cancel_inspect.example.txt b/config/cancel_inspect.example.txt deleted file mode 100644 index 7dab6c8..0000000 --- a/config/cancel_inspect.example.txt +++ /dev/null @@ -1 +0,0 @@ -mybank.com \ No newline at end of file diff --git a/config/custom_header.example.json b/config/custom_header.example.json deleted file mode 100644 index 171a54c..0000000 --- a/config/custom_header.example.json +++ /dev/null @@ -1,9 +0,0 @@ -{ - "https://example.com": { - "X-Custom-Header": "HeaderValue", - "X-Another-Header": "AnotherValue" - }, - "https://example2.com": { - "X-Different-Header": "DifferentValue" - } - } \ No newline at end of file diff --git a/config/shortcuts.example.txt b/config/shortcuts.example.txt deleted file mode 100644 index 2cefaf0..0000000 --- a/config/shortcuts.example.txt +++ /dev/null @@ -1,3 +0,0 @@ -f=https://facebook.com -gh=https://github.com -yt=https://youtube.com \ No newline at end of file diff --git a/docker-compose.yml b/docker-compose.yml index f3839dc..a10874b 100644 --- a/docker-compose.yml +++ b/docker-compose.yml @@ -12,9 +12,8 @@ services: #volumes: #- ./assets:/app/assets #- ./certs/ca:/app/certs/ca - #- ./config:/app/config #- ./logs:/app/logs - #- ./config.ini:/app/config.ini + #- ./config.yaml:/app/config.yaml environment: PYPROXY_HOST: 0.0.0.0 PYPROXY_PORT: 8080 diff --git a/pyproxy/handlers/http.py b/pyproxy/handlers/http.py index 5fc1e2c..6563383 100644 --- a/pyproxy/handlers/http.py +++ b/pyproxy/handlers/http.py @@ -6,7 +6,6 @@ """ import socket -import os import threading from urllib.parse import urlparse @@ -77,7 +76,7 @@ def _apply_shortcut(self, url: str) -> str | None: """ Checks if a shortcut is defined for the given domain. """ - if self.config_shortcuts and os.path.isfile(self.config_shortcuts): + if self.config_shortcuts: parsed_url = urlparse(url) domain = parsed_url.hostname self.shortcuts_queue.put(domain) @@ -142,7 +141,7 @@ def handle_http_request(self, client_socket, request): first_line = request.decode(errors="ignore").split("\n")[0] url = first_line.split(" ")[1] - if self.config_shortcuts and os.path.isfile(self.config_shortcuts): + if self.config_shortcuts: shortcut_url = self._apply_shortcut(url) if shortcut_url: response = ( @@ -158,7 +157,7 @@ def handle_http_request(self, client_socket, request): self._send_403(client_socket, url, first_line) return - if self.config_custom_header and os.path.isfile(self.config_custom_header): + if self.config_custom_header: request_text = request.decode(errors="ignore") request_lines = request_text.split("\r\n") headers = self._get_modified_headers(url, request_text) diff --git a/pyproxy/modules/cancel_inspect.py b/pyproxy/modules/cancel_inspect.py index aeeb2ca..005442c 100644 --- a/pyproxy/modules/cancel_inspect.py +++ b/pyproxy/modules/cancel_inspect.py @@ -2,19 +2,15 @@ pyproxy.modules.cancel_inspect.py This module contains functions and a process to load and monitor cancel inspection entries. -It reads a file containing cancel inspection data and checks whether specific entries exist -in that file. The file is monitored in a background thread for live updates. +It uses cancel inspection data from the configuration and checks whether specific entries exist +in that list. Functions: -- load_cancel_inspect: Loads the cancel inspection list from a file into a list. - cancel_inspect_process: Process that listens for URL-like entries and checks if they exist in the cancel inspection list. """ import multiprocessing -import time -import sys -import threading def load_cancel_inspect(cancel_inspect_path: str) -> dict: @@ -39,42 +35,20 @@ def load_cancel_inspect(cancel_inspect_path: str) -> dict: def cancel_inspect_process( queue: multiprocessing.Queue, result_queue: multiprocessing.Queue, - cancel_inspect_path: str, + cancel_inspect: list[str], ) -> None: """ - Process that monitors the cancel inspection file and checks if received entries exist in it. + Process that checks if received entries exist in the cancel inspection list. Args: queue (multiprocessing.Queue): A queue to receive entries to check. result_queue (multiprocessing.Queue): A queue to send back True/False depending on match. - cancel_inspect_path (str): Path to the file containing cancel inspection entries. + cancel_inspect (list[str]): The list of cancel inspection entries. """ - manager = multiprocessing.Manager() - cancel_inspect_data = manager.list(load_cancel_inspect(cancel_inspect_path)) - - error_event = threading.Event() - - def file_monitor() -> None: - try: - while True: - new_cancel_inspect = load_cancel_inspect(cancel_inspect_path) - cancel_inspect_data[:] = new_cancel_inspect - time.sleep(5) - except (IOError, ValueError) as e: - print(f"File monitor error: {e}") - error_event.set() - - monitor_thread = threading.Thread(target=file_monitor, daemon=True) - monitor_thread.start() - while True: - if error_event.is_set(): - print("Error detected in file monitor thread, terminating process.") - sys.exit(1) - try: url = queue.get() - if url in cancel_inspect_data: + if url in cancel_inspect: result_queue.put(True) else: result_queue.put(False) diff --git a/pyproxy/modules/custom_header.py b/pyproxy/modules/custom_header.py index 993b411..8a1ff35 100644 --- a/pyproxy/modules/custom_header.py +++ b/pyproxy/modules/custom_header.py @@ -2,76 +2,33 @@ pyproxy.modules.custom_header.py This module contains functions and a process to load and monitor custom header entries. -It reads a file with custom header data and checks if specific entries exist in it. -The file is monitored in a background thread for live updates. +It uses custom header data from the configuration and checks if specific entries exist in it. Functions: -- load_custom_header: Loads custom header entries from a file into a list. - custom_header_process: Process that listens for header-like entries and checks if they exist in the custom header list. """ import multiprocessing -import time -import sys -import threading -import json - - -def load_custom_header(custom_header_path: str) -> dict: - """ - Loads custom header entries from a file into a list. - - Args: - custom_header_path (str): The path to the file containing the custom headers. - - Returns: - dict: A dictionary containing the custom header data loaded from the file. - """ - with open(custom_header_path, "r", encoding="utf-8") as f: - return json.load(f) def custom_header_process( queue: multiprocessing.Queue, result_queue: multiprocessing.Queue, - custom_header_path: str, + custom_header: dict[str, dict[str, str]], ) -> None: """ - Process that monitors the custom header file and checks if received entries exist in it. + Process that checks if received entries exist in the custom header dict. Args: queue (multiprocessing.Queue): A queue to receive header-like entries to check. - result_queue (multiprocessing.Queue): A queue to send back True/False depending on match. - custom_header_path (str): Path to the file containing custom header entries. + result_queue (multiprocessing.Queue): A queue to send back the headers. + custom_header (dict[str, dict[str, str]]): The dict of custom headers. """ - manager = multiprocessing.Manager() - custom_header_data = manager.dict(load_custom_header(custom_header_path)) - - error_event = threading.Event() - - def file_monitor() -> None: - try: - while True: - new_custom_header = load_custom_header(custom_header_path) - custom_header_data.clear() - custom_header_data.update(new_custom_header) - time.sleep(5) - except (IOError, ValueError) as e: - print(f"File monitor error: {e}") - error_event.set() - - monitor_thread = threading.Thread(target=file_monitor, daemon=True) - monitor_thread.start() - while True: - if error_event.is_set(): - print("Error detected in file monitor thread, terminating process.") - sys.exit(1) - try: url = queue.get() - headers = custom_header_data.get(url, {}) + headers = custom_header.get(url, {}) result_queue.put(headers) except KeyboardInterrupt: diff --git a/pyproxy/modules/filter.py b/pyproxy/modules/filter.py index 41c5a4a..1058a41 100644 --- a/pyproxy/modules/filter.py +++ b/pyproxy/modules/filter.py @@ -2,72 +2,37 @@ pyproxy.modules.filter.py This module contains functions and a process to filter and block domains and URLs. -It loads blocked domain names and URLs from specified files, then listens for +It uses blocked domain names and URLs from the configuration, then listens for incoming requests to check if the domain or URL should be blocked. Functions: -- load_blacklist: Loads blocked FQDNs and URLs from files into sets for fast lookup. +- load_blacklist: Loads blocked FQDNs and URLs from lists into sets for fast lookup. - filter_process: The process that checks whether a domain or URL is blocked. """ import multiprocessing -import time -import sys -import threading from urllib.parse import urlparse -import requests -def load_blacklist(blocked_sites_path: str, blocked_url_path: str, filter_mode: str) -> set: +def load_blacklist(blocked_sites: list[str], blocked_url: list[str]) -> tuple[set, set]: """ - Loads blocked FQDNs or URLs from a file or URL into a set for fast lookup. + Loads blocked FQDNs and URLs from lists into sets for fast lookup. Args: - blocked_sites_path (str): The path or URL to the file containing blocked FQDNs. - blocked_url_path (str): The path or URL to the file containing blocked URLs. - filter_mode (str): Mode to determine if we load from local file or HTTP URL. + blocked_sites (list[str]): The list of blocked FQDNs. + blocked_url (list[str]): The list of blocked URLs. Returns: - set: A set of blocked domains/URLs. + tuple[set, set]: A tuple of sets of blocked domains and URLs. """ - blocked_sites = set() - blocked_url = set() - - def load_from_file(file_path: str) -> set: - data = set() - with open(file_path, "r", encoding="utf-8") as f: - for line in f: - data.add(line.strip()) - return data - - def load_from_http(url: str) -> set: - data = set() - try: - response = requests.get(url, timeout=3) - response.raise_for_status() - for line in response.text.splitlines(): - data.add(line.strip()) - except requests.exceptions.RequestException as e: - raise requests.exceptions.RequestException(f"Failed to load data from {url}: {e}") - return data - - if filter_mode == "local": - blocked_sites = load_from_file(blocked_sites_path) - blocked_url = load_from_file(blocked_url_path) - elif filter_mode == "http": - blocked_sites = load_from_http(blocked_sites_path) - blocked_url = load_from_http(blocked_url_path) - - return blocked_sites, blocked_url + return set(blocked_sites), set(blocked_url) def filter_process( queue: multiprocessing.Queue, result_queue: multiprocessing.Queue, - filter_mode: str, - blocked_sites_path: str, - blocked_url_path: str, - refresh_interval=5, + blocked_sites: list[str], + blocked_url: list[str], ) -> None: """ Process that listens for requests and checks if the domain/URL should be blocked. @@ -76,44 +41,12 @@ def filter_process( queue (multiprocessing.Queue): A queue to receive URL/domain for checking. result_queue (multiprocessing.Queue): A queue to send back the result of the filtering (blocked or allowed). - filter_mode (str): Filter list mode (local or http). - blocked_sites_path (str): The path to the file containing blocked FQDNs. - blocked_url_path (str): The path to the file containing blocked URLs. - refresh_interval (int): Interval in seconds to reload the blacklist files. + blocked_sites (list[str]): The list of blocked FQDNs. + blocked_url (list[str]): The list of blocked URLs. """ - manager = multiprocessing.Manager() - blocked_data = manager.dict( - { - "sites": load_blacklist(blocked_sites_path, blocked_url_path, filter_mode)[0], - "urls": load_blacklist(blocked_sites_path, blocked_url_path, filter_mode)[1], - } - ) - - error_event = threading.Event() - - def file_monitor() -> None: - try: - while True: - new_blocked_sites, new_blocked_url = load_blacklist( - blocked_sites_path, blocked_url_path, filter_mode - ) - - blocked_data["sites"] = new_blocked_sites - blocked_data["urls"] = new_blocked_url - - time.sleep(refresh_interval) - except (IOError, ValueError) as e: - print(f"File monitor error: {e}") - error_event.set() - - monitor_thread = threading.Thread(target=file_monitor, daemon=True) - monitor_thread.start() + blocked_sites_set, blocked_url_set = load_blacklist(blocked_sites, blocked_url) while True: - if error_event.is_set(): - print("Error detected in file monitor thread, terminating process.") - sys.exit(1) - try: request = queue.get() @@ -127,11 +60,11 @@ def file_monitor() -> None: server_host = parts[0] if parts else None full_url = server_host - if "*" in blocked_data["sites"] or any( - server_host.startswith(blocked_host) for blocked_host in blocked_data["sites"] + if "*" in blocked_sites_set or any( + server_host.startswith(blocked_host) for blocked_host in blocked_sites_set ): result_queue.put((server_host, "Blocked")) - elif any(full_url.startswith(blocked_url) for blocked_url in blocked_data["urls"]): + elif any(full_url.startswith(blocked_url) for blocked_url in blocked_url_set): result_queue.put((full_url, "Blocked")) else: result_queue.put((server_host, "Allowed")) diff --git a/pyproxy/modules/shortcuts.py b/pyproxy/modules/shortcuts.py index f917804..a2d33c5 100644 --- a/pyproxy/modules/shortcuts.py +++ b/pyproxy/modules/shortcuts.py @@ -6,14 +6,10 @@ a process that listens for requests to resolve an alias to its corresponding URL. Functions: -- load_shortcuts: Loads URL alias mappings from a file into a dictionary for fast lookup. - shortcuts_process: The process that listens for alias requests and resolves them to URLs. """ import multiprocessing -import time -import sys -import threading def load_shortcuts(shortcuts_path: str) -> dict: @@ -41,7 +37,7 @@ def load_shortcuts(shortcuts_path: str) -> dict: def shortcuts_process( queue: multiprocessing.Queue, result_queue: multiprocessing.Queue, - shortcuts_path: str, + shortcuts: dict[str, str], ) -> None: """ Process that listens for alias requests and resolves them to URLs. @@ -49,36 +45,12 @@ def shortcuts_process( Args: queue (multiprocessing.Queue): A queue to receive alias for URL resolution. result_queue (multiprocessing.Queue): A queue to send back the resolved URL. - shortcuts_path (str): The path to the file containing alias=URL mappings. + shortcuts (dict[str, str]): The dictionary of alias to URL mappings. """ - manager = multiprocessing.Manager() - shortcuts_data = manager.dict({"shortcuts": load_shortcuts(shortcuts_path)}) - - error_event = threading.Event() - - def file_monitor() -> None: - try: - while True: - new_shortcuts = load_shortcuts(shortcuts_path) - - shortcuts_data["shortcuts"] = new_shortcuts - - time.sleep(5) - except (IOError, ValueError) as e: - print(f"File monitor error: {e}") - error_event.set() - - monitor_thread = threading.Thread(target=file_monitor, daemon=True) - monitor_thread.start() - while True: - if error_event.is_set(): - print("Error detected in file monitor thread, terminating process.") - sys.exit(1) - try: alias = queue.get() - url = shortcuts_data["shortcuts"].get(alias) + url = shortcuts.get(alias) result_queue.put(url) except KeyboardInterrupt: diff --git a/pyproxy/pyproxy.py b/pyproxy/pyproxy.py index 92c6b3e..b7114ea 100644 --- a/pyproxy/pyproxy.py +++ b/pyproxy/pyproxy.py @@ -25,43 +25,39 @@ def main(): config = load_config(args.config_file) main_config = ProxyConfigMain( - host=get_config_value(args, config, "host", "Server", "0.0.0.0"), # noqa: S104 - port=int(get_config_value(args, config, "port", "Server", 8080)), - debug=str_to_bool(get_config_value(args, config, "debug", "Logging", False)), - html_403=get_config_value(args, config, "html_403", "Files", "assets/403.html"), - shortcuts=get_config_value(args, config, "shortcuts", "Options", "config/shortcuts.txt"), - custom_header=get_config_value( - args, config, "custom_header", "Options", "config/custom_header.json" - ), - authorized_ips=get_config_value( - args, config, "authorized_ips", "Options", "config/authorized_ips.txt" - ), + host=get_config_value(args, config, "host", "server", "0.0.0.0"), # noqa: S104 + port=int(get_config_value(args, config, "port", "server", 8080)), + debug=str_to_bool(get_config_value(args, config, "debug", "logging", False)), + html_403=get_config_value(args, config, "html_403", "files", "assets/403.html"), + shortcuts=config.get("options", {}).get("shortcuts", {}), + custom_header=config.get("options", {}).get("custom_header", {}), + authorized_ips=config.get("options", {}).get("authorized_ips", []), ) monitoring_config = ProxyConfigMonitoring( - flask_port=get_config_value(args, config, "flask_port", "Monitoring", 5000), - flask_pass=get_config_value(args, config, "flask_pass", "Monitoring", "password"), + flask_port=get_config_value(args, config, "flask_port", "monitoring", 5000), + flask_pass=get_config_value(args, config, "flask_pass", "monitoring", "password"), ) proxy_config = ProxyConfigProxy( - enable=str_to_bool(get_config_value(args, config, "proxy_enable", "Proxy", False)), - host=get_config_value(args, config, "proxy_host", "Proxy", "127.0.0.1"), - port=get_config_value(args, config, "proxy_port", "Proxy", 8081), + enable=str_to_bool(get_config_value(args, config, "proxy_enable", "proxy", False)), + host=get_config_value(args, config, "proxy_host", "proxy", "127.0.0.1"), + port=get_config_value(args, config, "proxy_port", "proxy", 8081), ) - console_format = config.get("Logging", "console_format", fallback=None) - access_log_format = config.get("Logging", "access_log_format", fallback=None) - block_log_format = config.get("Logging", "block_log_format", fallback=None) - datefmt = config.get("Logging", "datefmt", fallback=None) + console_format = config.get("logging", {}).get("console_format") + access_log_format = config.get("logging", {}).get("access_log_format") + block_log_format = config.get("logging", {}).get("block_log_format") + datefmt = config.get("logging", {}).get("datefmt") logger_config = ProxyConfigLogger( - access_log=get_config_value(args, config, "access_log", "Logging", "logs/access.log"), - block_log=get_config_value(args, config, "block_log", "Logging", "logs/block.log"), + access_log=get_config_value(args, config, "access_log", "logging", "logs/access.log"), + block_log=get_config_value(args, config, "block_log", "logging", "logs/block.log"), no_logging_access=str_to_bool( - get_config_value(args, config, "no_logging_access", "Logging", False) + get_config_value(args, config, "no_logging_access", "logging", False) ), no_logging_block=str_to_bool( - get_config_value(args, config, "no_logging_block", "Logging", False) + get_config_value(args, config, "no_logging_block", "logging", False) ), console_format=( console_format @@ -107,30 +103,24 @@ def main(): ) filter_config = ProxyConfigFilter( - no_filter=str_to_bool(get_config_value(args, config, "no_filter", "Filtering", False)), - filter_mode=get_config_value(args, config, "filter_mode", "Filtering", "local"), - blocked_sites=get_config_value( - args, config, "blocked_sites", "Filtering", "config/blocked_sites.txt" - ), - blocked_url=get_config_value( - args, config, "blocked_url", "Filtering", "config/blocked_url.txt" - ), + no_filter=str_to_bool(get_config_value(args, config, "no_filter", "filtering", False)), + filter_mode=get_config_value(args, config, "filter_mode", "filtering", "local"), + blocked_sites=config.get("filtering", {}).get("blocked_sites", []), + blocked_url=config.get("filtering", {}).get("blocked_url", []), ) ssl_config = ProxyConfigSSL( - ssl_inspect=str_to_bool(get_config_value(args, config, "ssl_inspect", "Security", False)), + ssl_inspect=str_to_bool(get_config_value(args, config, "ssl_inspect", "security", False)), inspect_ca_cert=get_config_value( - args, config, "inspect_ca_cert", "Security", "certs/ca/cert.pem" + args, config, "inspect_ca_cert", "security", "certs/ca/cert.pem" ), inspect_ca_key=get_config_value( - args, config, "inspect_ca_key", "Security", "certs/ca/key.pem" + args, config, "inspect_ca_key", "security", "certs/ca/key.pem" ), inspect_certs_folder=get_config_value( - args, config, "inspect_certs_folder", "Security", "certs/" - ), - cancel_inspect=get_config_value( - args, config, "cancel_inspect", "Security", "config/cancel_inspect.txt" + args, config, "inspect_certs_folder", "security", "certs/" ), + cancel_inspect=config.get("security", {}).get("cancel_inspect", []), ) proxy = ProxyServer( diff --git a/pyproxy/server.py b/pyproxy/server.py index c1a3927..879883a 100644 --- a/pyproxy/server.py +++ b/pyproxy/server.py @@ -133,7 +133,6 @@ def _initialize_processes(self): args=( self.filter_queue, self.filter_result_queue, - self.filter_config.filter_mode, self.filter_config.blocked_sites, self.filter_config.blocked_url, ), @@ -141,7 +140,7 @@ def _initialize_processes(self): self.filter_proc.start() self.console_logger.debug("[*] Starting the filter process...") - if not __slim__ and self.config_shortcuts and os.path.isfile(self.config_shortcuts): + if not __slim__ and self.config_shortcuts: self.shortcuts_proc = multiprocessing.Process( target=shortcuts_process, args=( @@ -153,7 +152,7 @@ def _initialize_processes(self): self.shortcuts_proc.start() self.console_logger.debug("[*] Starting the shortcuts process...") - if self.ssl_config.cancel_inspect and os.path.isfile(self.ssl_config.cancel_inspect): + if self.ssl_config.cancel_inspect: self.cancel_inspect_proc = multiprocessing.Process( target=cancel_inspect_process, args=( @@ -165,7 +164,7 @@ def _initialize_processes(self): self.cancel_inspect_proc.start() self.console_logger.debug("[*] Starting the cancel inspection process...") - if not __slim__ and self.config_custom_header and os.path.isfile(self.config_custom_header): + if not __slim__ and self.config_custom_header: self.custom_header_proc = multiprocessing.Process( target=custom_header_process, args=( @@ -191,20 +190,19 @@ def _clean_inspection_folder(self): def _load_authorized_ips(self): """ - Load authorized IPs/subnets from the file. + Load authorized IPs/subnets from the config. """ self.allowed_subnets = None - if self.authorized_ips and os.path.isfile(self.authorized_ips): - with open(self.authorized_ips, "r", encoding="utf-8") as f: - lines = [line.strip() for line in f if line.strip()] + if self.authorized_ips: + lines = self.authorized_ips try: self.allowed_subnets = [ipaddress.ip_network(line, strict=False) for line in lines] self.console_logger.debug( "[*] Loaded %d authorized IPs/subnets", len(self.allowed_subnets) ) except ValueError as e: - self.console_logger.error("[*] Invalid IP/subnet in %s: %s", self.authorized_ips, e) + self.console_logger.error("[*] Invalid IP/subnet in config: %s", e) self.allowed_subnets = None def _validate_ssl_inspection_files(self): @@ -265,15 +263,6 @@ def start(self): self._validate_ssl_inspection_files() self._clean_inspection_folder() - if self.filter_config.filter_mode == "local": - for file in [ - self.filter_config.blocked_sites, - self.filter_config.blocked_url, - ]: - if not os.path.exists(file): - with open(file, "w", encoding="utf-8"): - pass - self._initialize_processes() self._load_authorized_ips() diff --git a/pyproxy/utils/args.py b/pyproxy/utils/args.py index afc6b0e..df46e3a 100644 --- a/pyproxy/utils/args.py +++ b/pyproxy/utils/args.py @@ -4,11 +4,11 @@ This module allows you to read the program configuration file and return the values. """ -import configparser import argparse import os from rich_argparse import MetavarTypeRichHelpFormatter from pyproxy import __version__ +import yaml def parse_args() -> argparse.Namespace: @@ -35,8 +35,8 @@ def parse_args() -> argparse.Namespace: "-f", "--config-file", type=str, - default="./config.ini", - help="Path to config.ini file", + default="./config.yaml", + help="Path to config.yaml file", ) # noqa: E501 parser.add_argument("--access-log", type=str, help="Path to the access log file") parser.add_argument("--block-log", type=str, help="Path to the block log file") @@ -94,24 +94,24 @@ def parse_args() -> argparse.Namespace: return parser.parse_args() -def load_config(config_path: str) -> configparser.ConfigParser: +def load_config(config_path: str) -> dict: """ - Loads the configuration file and returns the parsed config object. + Loads the configuration file and returns the parsed config dict. Args: config_path (str): The path to the configuration file to load. Returns: - configparser.ConfigParser: The parsed configuration object. + dict: The parsed configuration dict. """ - config = configparser.ConfigParser(interpolation=None) - config.read(config_path) + with open(config_path, "r") as f: + config = yaml.safe_load(f) return config def get_config_value( args: argparse.Namespace, - config: configparser.ConfigParser, + config: dict, arg_name: str, section: str, fallback_value: str, @@ -122,9 +122,9 @@ def get_config_value( Args: args (argparse.Namespace): The parsed command-line arguments object. - config (configparser.ConfigParser): The parsed configuration object. + config (dict): The parsed configuration dict. arg_name (str): The name of the command-line argument. - section (str): The section in the config file where the value is located. + section (str): The section in the config dict where the value is located. fallback_value (str): The fallback value to return if neither argument nor config has a value. @@ -140,7 +140,10 @@ def get_config_value( if env_value: return env_value - return config.get(section, arg_name, fallback=fallback_value) + try: + return config[section][arg_name] + except KeyError: + return fallback_value def str_to_bool(value: str) -> bool: diff --git a/pyproxy/utils/config.py b/pyproxy/utils/config.py index 85b7ae3..9fb88eb 100644 --- a/pyproxy/utils/config.py +++ b/pyproxy/utils/config.py @@ -17,9 +17,9 @@ class ProxyConfigMain: port: int debug: bool html_403: str - shortcuts: str - custom_header: str - authorized_ips: str + shortcuts: dict[str, str] + custom_header: dict[str, dict[str, str]] + authorized_ips: list[str] def to_dict(self): return asdict(self) @@ -79,8 +79,8 @@ class ProxyConfigFilter: no_filter: bool filter_mode: str - blocked_sites: str - blocked_url: str + blocked_sites: list[str] + blocked_url: list[str] def to_dict(self): return asdict(self) @@ -96,7 +96,7 @@ class ProxyConfigSSL: inspect_ca_cert: str inspect_ca_key: str inspect_certs_folder: str - cancel_inspect: str + cancel_inspect: list[str] def to_dict(self): return asdict(self) diff --git a/requirements.txt b/requirements.txt index 6029160..abe2b49 100644 --- a/requirements.txt +++ b/requirements.txt @@ -6,3 +6,4 @@ Flask-HTTPAuth>=4.8.0 Flask-Babel>=4.0.0 psutil>=7.1.0 colorlog>=6.9.0 +PyYAML>=6.0.3 diff --git a/tests/modules/test_cancel_inspect.py b/tests/modules/test_cancel_inspect.py index 3709f8a..1bd8938 100644 --- a/tests/modules/test_cancel_inspect.py +++ b/tests/modules/test_cancel_inspect.py @@ -15,7 +15,7 @@ import multiprocessing import time -from pyproxy.modules.cancel_inspect import load_cancel_inspect, cancel_inspect_process +from pyproxy.modules.cancel_inspect import cancel_inspect_process class TestCancelInspect(unittest.TestCase): @@ -33,29 +33,28 @@ def tearDown(self): os.unlink(self.path) def test_load_cancel_inspect(self): - """Test that the cancel inspection file is correctly loaded into a list.""" - entries = load_cancel_inspect(self.path) - self.assertEqual(len(entries), 2) - self.assertIn("http://example.com/1\n", entries) - self.assertIn("http://example.com/2\n", entries) + """Test that the cancel inspection list is correctly used.""" + # Since we embed, no load function + pass def test_cancel_inspect_process(self): """Test that the cancel inspection process returns the correct match result.""" queue = multiprocessing.Queue() result_queue = multiprocessing.Queue() + cancel_list = ["http://example.com/1", "http://example.com/2"] process = multiprocessing.Process( - target=cancel_inspect_process, args=(queue, result_queue, self.path) + target=cancel_inspect_process, args=(queue, result_queue, cancel_list) ) process.start() time.sleep(1) - queue.put("http://example.com/1\n") + queue.put("http://example.com/1") result = result_queue.get(timeout=3) self.assertTrue(result) - queue.put("http://nonexistent.com/\n") + queue.put("http://nonexistent.com/") result = result_queue.get(timeout=3) self.assertFalse(result) diff --git a/tests/modules/test_custom_header.py b/tests/modules/test_custom_header.py index 27a413e..fa268ac 100644 --- a/tests/modules/test_custom_header.py +++ b/tests/modules/test_custom_header.py @@ -15,7 +15,7 @@ import time import json -from pyproxy.modules.custom_header import load_custom_header, custom_header_process +from pyproxy.modules.custom_header import custom_header_process class TestCustomHeader(unittest.TestCase): @@ -37,10 +37,9 @@ def tearDown(self): os.unlink(self.path) def test_load_custom_header(self): - """Test that the custom header JSON file is correctly loaded into a dictionary.""" - headers = load_custom_header(self.path) - self.assertEqual(headers["http://example.com"]["X-Test-Header"], "123") - self.assertIn("http://another.com", headers) + """Test that the custom header dict is correctly used.""" + # Since we embed, no load function + pass def test_custom_header_process(self): """Test that the custom header process returns the correct header dictionary.""" @@ -48,7 +47,7 @@ def test_custom_header_process(self): result_queue = multiprocessing.Queue() process = multiprocessing.Process( - target=custom_header_process, args=(queue, result_queue, self.path) + target=custom_header_process, args=(queue, result_queue, self.sample_data) ) process.start() diff --git a/tests/modules/test_filter.py b/tests/modules/test_filter.py index 338aa82..acbf6f4 100644 --- a/tests/modules/test_filter.py +++ b/tests/modules/test_filter.py @@ -22,7 +22,7 @@ import unittest import multiprocessing -from unittest.mock import patch, mock_open +from unittest.mock import patch import requests from pyproxy.modules.filter import load_blacklist, filter_process @@ -45,25 +45,12 @@ def tearDown(self): self.result_queue.get_nowait() def test_load_blacklist(self): - """Tests if the blacklist is correctly loaded from the file.""" - with patch( - "builtins.open", - new_callable=mock_open, - read_data="blocked.com\nallowed.com/blocked", - ): - blocked_sites, blocked_urls = load_blacklist( - "blocked_sites.txt", "blocked_urls.txt", "local" - ) - self.assertIn("blocked.com", blocked_sites) - self.assertIn("allowed.com/blocked", blocked_sites) - self.assertIsInstance(blocked_sites, set) - self.assertIsInstance(blocked_urls, set) - - @patch("builtins.open", side_effect=FileNotFoundError("File not found")) - def test_load_blacklist_file_not_found(self, _mock_file): - """Tests that a FileNotFoundError is raised when the blacklist file is missing.""" - with self.assertRaises(FileNotFoundError): - load_blacklist("invalid_file.txt", "blocked_urls.txt", "local") + """Tests if the blacklist is correctly loaded from the lists.""" + blocked_sites, blocked_urls = load_blacklist(["blocked.com", "allowed.com/blocked"], []) + self.assertIn("blocked.com", blocked_sites) + self.assertIn("allowed.com/blocked", blocked_sites) + self.assertIsInstance(blocked_sites, set) + self.assertIsInstance(blocked_urls, set) @patch( "requests.get", @@ -71,17 +58,12 @@ def test_load_blacklist_file_not_found(self, _mock_file): ) def test_load_blacklist_http_error(self, _mock_request): """Tests that an HTTP error is handled correctly when loading blacklists.""" - with self.assertRaises(requests.exceptions.RequestException): - load_blacklist( - "http://example.com/blocked_sites", - "http://example.com/blocked_urls", - "http", - ) - - @patch("builtins.open", new_callable=mock_open, read_data="") - def test_load_blacklist_empty_file(self, _mock_file): - """Tests that an empty file returns empty sets for blocked sites and URLs.""" - blocked_sites, blocked_urls = load_blacklist("empty_sites.txt", "empty_urls.txt", "local") + # Since we removed http mode, this test is obsolete + pass + + def test_load_blacklist_empty(self): + """Tests that empty lists return empty sets for blocked sites and URLs.""" + blocked_sites, blocked_urls = load_blacklist([], []) self.assertEqual(len(blocked_sites), 0) self.assertEqual(len(blocked_urls), 0) @@ -89,32 +71,35 @@ def _test_filter_process_helper( self, input_urls, expected_results, - patch_data="blocked.com\nallowed.com/blocked", + blocked_sites=None, + blocked_urls=None, ): """Helper method to test filter_process with different inputs.""" - with patch("builtins.open", new_callable=mock_open, read_data=patch_data): - process = multiprocessing.Process( - target=filter_process, - args=( - self.queue, - self.result_queue, - "local", - "blocked_sites.txt", - "blocked_urls.txt", - ), - ) - process.start() - - for url in input_urls: - self.queue.put(url) - - results = [] - for _ in expected_results: - results.append(self.result_queue.get(timeout=2)) - - self.assertEqual(results, expected_results) - process.terminate() - process.join() + if blocked_sites is None: + blocked_sites = ["blocked.com", "allowed.com/blocked"] + if blocked_urls is None: + blocked_urls = [] + process = multiprocessing.Process( + target=filter_process, + args=( + self.queue, + self.result_queue, + blocked_sites, + blocked_urls, + ), + ) + process.start() + + for url in input_urls: + self.queue.put(url) + + results = [] + for _ in expected_results: + results.append(self.result_queue.get(timeout=2)) + + self.assertEqual(results, expected_results) + process.terminate() + process.join() def test_filter_process(self): """Tests if domains/URLs are correctly identified as blocked or allowed.""" @@ -154,14 +139,16 @@ def test_filter_process_subdomain_not_blocked(self): """ input_urls = ["http://sub.blocked.com/"] expected_results = [("sub.blocked.com", "Allowed")] - self._test_filter_process_helper(input_urls, expected_results, patch_data="blocked.com\n") + self._test_filter_process_helper( + input_urls, expected_results, blocked_sites=["blocked.com"] + ) def test_filter_process_special_characters(self): """Tests if URLs with special characters are correctly handled.""" input_urls = ["http://weird-site.com/"] expected_results = [("weird-site.com", "Blocked")] self._test_filter_process_helper( - input_urls, expected_results, patch_data="weird-site.com\n" + input_urls, expected_results, blocked_sites=["weird-site.com"] ) def test_filter_process_with_path_and_port(self): diff --git a/tests/modules/test_shortcuts.py b/tests/modules/test_shortcuts.py index 3438085..2cd8eb3 100644 --- a/tests/modules/test_shortcuts.py +++ b/tests/modules/test_shortcuts.py @@ -23,8 +23,7 @@ import unittest import multiprocessing -from unittest.mock import patch, mock_open -from pyproxy.modules.shortcuts import load_shortcuts, shortcuts_process +from pyproxy.modules.shortcuts import shortcuts_process class TestShortcuts(unittest.TestCase): @@ -44,42 +43,23 @@ def tearDown(self): while not self.result_queue.empty(): self.result_queue.get_nowait() - def test_load_shortcuts(self): - """Tests if the shortcuts are correctly loaded from the file.""" - with patch( - "builtins.open", - new_callable=mock_open, - read_data="alias1=http://example.com\nalias2=http://test.com", - ): - shortcuts = load_shortcuts("shortcuts.txt") - self.assertEqual(shortcuts["alias1"], "http://example.com") - self.assertEqual(shortcuts["alias2"], "http://test.com") - self.assertIsInstance(shortcuts, dict) - - @patch("builtins.open", side_effect=FileNotFoundError("File not found")) - def test_load_shortcuts_file_not_found(self, _mock_file): - """Tests that a FileNotFoundError is raised when the shortcuts file is missing.""" - with self.assertRaises(FileNotFoundError): - load_shortcuts("invalid_file.txt") - - def _test_shortcuts_process_helper( - self, alias, expected_url, patch_data="alias1=http://example.com" - ): + def _test_shortcuts_process_helper(self, alias, expected_url, shortcuts_dict=None): """Helper method to test shortcuts_process with different alias requests.""" - with patch("builtins.open", new_callable=mock_open, read_data=patch_data): - process = multiprocessing.Process( - target=shortcuts_process, - args=(self.queue, self.result_queue, "shortcuts.txt"), - ) - process.start() + if shortcuts_dict is None: + shortcuts_dict = {"alias1": "http://example.com"} + process = multiprocessing.Process( + target=shortcuts_process, + args=(self.queue, self.result_queue, shortcuts_dict), + ) + process.start() - self.queue.put(alias) + self.queue.put(alias) - result = self.result_queue.get(timeout=2) - self.assertEqual(result, expected_url) + result = self.result_queue.get(timeout=2) + self.assertEqual(result, expected_url) - process.terminate() - process.join() + process.terminate() + process.join() def test_shortcuts_process(self): """Tests if alias requests are correctly resolved to URLs.""" @@ -91,28 +71,24 @@ def test_shortcuts_process_invalid_alias(self): def test_shortcuts_process_with_multiple_aliases(self): """Tests if multiple alias requests are correctly resolved.""" - with patch( - "builtins.open", - new_callable=mock_open, - read_data="alias1=http://example.com\nalias2=http://test.com", - ): - process = multiprocessing.Process( - target=shortcuts_process, - args=(self.queue, self.result_queue, "shortcuts.txt"), - ) - process.start() - - self.queue.put("alias1") - self.queue.put("alias2") - - result1 = self.result_queue.get(timeout=2) - result2 = self.result_queue.get(timeout=2) - - self.assertEqual(result1, "http://example.com") - self.assertEqual(result2, "http://test.com") - - process.terminate() - process.join() + shortcuts_dict = {"alias1": "http://example.com", "alias2": "http://test.com"} + process = multiprocessing.Process( + target=shortcuts_process, + args=(self.queue, self.result_queue, shortcuts_dict), + ) + process.start() + + self.queue.put("alias1") + self.queue.put("alias2") + + result1 = self.result_queue.get(timeout=2) + result2 = self.result_queue.get(timeout=2) + + self.assertEqual(result1, "http://example.com") + self.assertEqual(result2, "http://test.com") + + process.terminate() + process.join() if __name__ == "__main__": From ac8db284e611bfd220b32e351720d4109ea3d158 Mon Sep 17 00:00:00 2001 From: 6C656C65 <73671374+6C656C65@users.noreply.github.com> Date: Sun, 9 Nov 2025 19:54:54 +0100 Subject: [PATCH 3/4] Transfert wiki to gh-pages --- README.md | 2 +- pyproject.toml | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/README.md b/README.md index c3ac480..2cf3ed7 100644 --- a/README.md +++ b/README.md @@ -76,7 +76,7 @@ The proxy will be available at: `0.0.0.0:8080`. The access log will be available at `./logs/access.log`. ## 📚 **Documentation** -If you encounter any problems, or if you want to use the program in a particular way, I advise you to read the [documentation](https://github.com/pyproxytools/pyproxy/wiki). +If you encounter any problems, or if you want to use the program in a particular way, I advise you to read the [documentation](https://pyproxytools.github.io/pyproxy-docs/). ## 🔧 **To do** diff --git a/pyproject.toml b/pyproject.toml index 2957332..a8df55a 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -27,7 +27,7 @@ classifiers = [ dynamic = ["version", "dependencies"] [project.urls] -Documentation = "https://github.com/pyproxytools/pyproxy/wiki" +Documentation = "https://pyproxytools.github.io/pyproxy-docs/" "Issue tracker" = "https://github.com/pyproxytools/pyproxy/issues" [tool.setuptools.packages] From 19d59a6847454a619abcd7d8e82986f3597d23dd Mon Sep 17 00:00:00 2001 From: 6C656C65 <73671374+6C656C65@users.noreply.github.com> Date: Thu, 30 Apr 2026 18:15:08 +0200 Subject: [PATCH 4/4] fix: handle IPv6 getpeername tuple (#20) --- pyproxy/handlers/https.py | 11 ++++++++--- 1 file changed, 8 insertions(+), 3 deletions(-) diff --git a/pyproxy/handlers/https.py b/pyproxy/handlers/https.py index 34b5257..2973b1e 100644 --- a/pyproxy/handlers/https.py +++ b/pyproxy/handlers/https.py @@ -6,9 +6,9 @@ relay raw data when SSL inspection is disabled. """ -import socket -import select import os +import select +import socket import ssl import threading @@ -377,7 +377,12 @@ def transfer_data_between_sockets(self, client_socket, server_socket): and "target_ip" not in self.active_connections[thread_id] ): try: - target_ip, target_port = server_socket.getpeername() + peer = server_socket.getpeername() + if len(peer) == 2: + target_ip, target_port = server_socket.getpeername() + else: + target_ip, target_port, *_ = server_socket.getpeername() + self.active_connections[thread_id]["target_ip"] = target_ip self.active_connections[thread_id]["target_port"] = target_port except OSError as e: