diff --git a/.bumpversion.cfg b/.bumpversion.cfg index 7d43a83..8d3eec7 100644 --- a/.bumpversion.cfg +++ b/.bumpversion.cfg @@ -1,5 +1,5 @@ [bumpversion] -current_version = 0.3.1 +current_version = 0.3.2 commit = True tag = True diff --git a/README.md b/README.md index 97700b4..a00e8df 100644 --- a/README.md +++ b/README.md @@ -37,6 +37,7 @@ | Custom headers | ✅ | | Web interface monitoring | ✅ | | Lightweight Docker image | ✅ | +| Proxy chaining (multi-proxy forwarding) | ✅ | ## 📦 **Installation** diff --git a/config.ini.example b/config.ini.example index 0955071..30192e6 100644 --- a/config.ini.example +++ b/config.ini.example @@ -31,4 +31,9 @@ cancel_inspect = config/cancel_inspect.txt [Monitoring] flask_port = 5000 -flask_pass = password \ No newline at end of file +flask_pass = password + +[Proxy] +proxy_enable = false +proxy_host = 127.0.0.1 +proxy_port = 8081 diff --git a/pyproxy.py b/pyproxy.py index 61a3ea6..6ef6fd4 100644 --- a/pyproxy.py +++ b/pyproxy.py @@ -8,7 +8,7 @@ from pyproxy.utils.args import parse_args, load_config, get_config_value from pyproxy.utils.config import ProxyConfigLogger, ProxyConfigFilter, ProxyConfigSSL -# pylint: disable=C0301 +# pylint: disable=C0301,R0914 def main(): """ Main entry point of the proxy server. It parses command-line arguments, loads the configuration file, @@ -25,6 +25,9 @@ def main(): custom_header = get_config_value(args, config, 'custom_header', 'Options', "config/custom_header.json") flask_port = get_config_value(args, config, 'flask_port', 'Monitoring', 5000) flask_pass = get_config_value(args, config, 'flask_pass', 'Monitoring', "password") + proxy_enable = get_config_value(args, config, 'proxy_enable', 'Proxy', False) + proxy_host = get_config_value(args, config, 'proxy_host', 'Proxy', "127.0.0.1") + proxy_port = get_config_value(args, config, 'proxy_port', 'Proxy', 8081) logger_config = ProxyConfigLogger( access_log=get_config_value(args, config, 'access_log', 'Logging', "logs/access.log"), @@ -59,7 +62,10 @@ def main(): flask_pass=flask_pass, html_403=html_403, shortcuts=shortcuts, - custom_header=custom_header + custom_header=custom_header, + proxy_enable=proxy_enable, + proxy_host=proxy_host, + proxy_port=proxy_port ) proxy.start() diff --git a/pyproxy/handlers/__init__.py b/pyproxy/handlers/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/pyproxy/handlers/client.py b/pyproxy/handlers/client.py new file mode 100644 index 0000000..8c494f2 --- /dev/null +++ b/pyproxy/handlers/client.py @@ -0,0 +1,108 @@ +""" +pyproxy.handlers.client.py + +This module defines the ProxyHandlers class used by the proxy server to process +HTTP and HTTPS client connections. It handles request forwarding, blocking, shortcut +redirection, custom headers, and optional SSL inspection. +""" + +import threading + +from pyproxy.handlers.http import HttpHandler +from pyproxy.handlers.https import HttpsHandler + +# pylint: disable=R0914,R0903 +class ProxyHandlers: + """ + ProxyHandlers manages client connections for a proxy server, handling both HTTP + and HTTPS requests. It processes request forwarding, blocking, SSL inspection, + and custom headers based on configuration settings. This class is responsible + for dispatching the correct handler for HTTP or HTTPS requests and managing + connection-related operations. + """ + def __init__(self, html_403, logger_config, filter_config, ssl_config, + filter_queue, filter_result_queue, shortcuts_queue, shortcuts_result_queue, + cancel_inspect_queue, cancel_inspect_result_queue, custom_header_queue, + custom_header_result_queue, console_logger, shortcuts, custom_header, + active_connections, proxy_enable, proxy_host, proxy_port): + self.html_403 = html_403 + self.logger_config = logger_config + self.filter_config = filter_config + self.ssl_config = ssl_config + self.filter_queue = filter_queue + self.filter_result_queue = filter_result_queue + self.shortcuts_queue = shortcuts_queue + self.shortcuts_result_queue = shortcuts_result_queue + self.cancel_inspect_queue = cancel_inspect_queue + self.cancel_inspect_result_queue = cancel_inspect_result_queue + self.custom_header_queue = custom_header_queue + self.custom_header_result_queue = custom_header_result_queue + self.console_logger = console_logger + self.config_shortcuts = shortcuts + self.config_custom_header = custom_header + self.proxy_enable=proxy_enable + self.proxy_host=proxy_host + self.proxy_port=proxy_port + self.active_connections = active_connections + + def handle_client(self, client_socket): + """ + Handles an incoming client connection by processing the request and forwarding + it to the appropriate handler based on whether the request is HTTP or HTTPS. + + Args: + client_socket (socket): The socket object for the client connection. + """ + request = client_socket.recv(4096) + + 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 first_line.startswith("CONNECT"): + client_https_handler = HttpsHandler( + html_403=self.html_403, + logger_config=self.logger_config, + filter_config=self.filter_config, + ssl_config=self.ssl_config, + filter_queue=self.filter_queue, + filter_result_queue=self.filter_result_queue, + shortcuts_queue=self.shortcuts_queue, + shortcuts_result_queue=self.shortcuts_result_queue, + cancel_inspect_queue=self.cancel_inspect_queue, + cancel_inspect_result_queue=self.cancel_inspect_result_queue, + custom_header_queue=self.custom_header_queue, + custom_header_result_queue=self.custom_header_result_queue, + console_logger=self.console_logger, + shortcuts=self.config_shortcuts, + custom_header=self.config_custom_header, + proxy_enable=self.proxy_enable, + proxy_host=self.proxy_host, + proxy_port=self.proxy_port, + active_connections=self.active_connections + ) + client_https_handler.handle_https_connection(client_socket, first_line) + else: + client_http_handler = HttpHandler( + html_403=self.html_403, + logger_config=self.logger_config, + filter_config=self.filter_config, + filter_queue=self.filter_queue, + filter_result_queue=self.filter_result_queue, + shortcuts_queue=self.shortcuts_queue, + shortcuts_result_queue=self.shortcuts_result_queue, + custom_header_queue=self.custom_header_queue, + custom_header_result_queue=self.custom_header_result_queue, + console_logger=self.console_logger, + shortcuts=self.config_shortcuts, + custom_header=self.config_custom_header, + proxy_enable=self.proxy_enable, + proxy_host=self.proxy_host, + proxy_port=self.proxy_port, + active_connections=self.active_connections + ) + client_http_handler.handle_http_request(client_socket, request) diff --git a/pyproxy/handlers/http.py b/pyproxy/handlers/http.py new file mode 100644 index 0000000..7d260cb --- /dev/null +++ b/pyproxy/handlers/http.py @@ -0,0 +1,178 @@ +""" +pyproxy.handlers.http.py + +This module defines the HttpHandler class used by the proxy server to process +HTTP client connections. It handles request forwarding, blocking, and custom headers. +""" + +import socket +import os +import threading + +from pyproxy.utils.http_req import extract_headers, parse_url + +# pylint: disable=R0914 +class HttpHandler: + """ + HttpHandler manages client HTTP connections for a proxy server, + handling request forwarding, filtering, blocking, and custom header modification + based on configuration settings. + """ + def __init__(self, html_403, logger_config, filter_config, + filter_queue, filter_result_queue, shortcuts_queue, shortcuts_result_queue, + custom_header_queue, custom_header_result_queue, console_logger, shortcuts, + custom_header, active_connections, proxy_enable, proxy_host, proxy_port): + self.html_403 = html_403 + self.logger_config = logger_config + self.filter_config = filter_config + self.filter_queue = filter_queue + self.filter_result_queue = filter_result_queue + self.shortcuts_queue = shortcuts_queue + self.shortcuts_result_queue = shortcuts_result_queue + self.custom_header_queue = custom_header_queue + self.custom_header_result_queue = custom_header_result_queue + self.console_logger = console_logger + self.config_shortcuts = shortcuts + self.config_custom_header = custom_header + self.proxy_enable=proxy_enable + self.proxy_host=proxy_host + self.proxy_port=proxy_port + self.active_connections = active_connections + + def handle_http_request(self, client_socket, request): + """ + Processes an HTTP request, checks for URL filtering, applies shortcuts, + and forwards the request to the target server if not blocked. + + Args: + client_socket (socket): The socket object for the client connection. + request (bytes): The raw HTTP request sent by the client. + """ + first_line = request.decode(errors='ignore').split("\n")[0] + url = first_line.split(" ")[1] + + if self.config_custom_header and os.path.isfile(self.config_custom_header): + headers = extract_headers(request.decode(errors='ignore')) + self.custom_header_queue.put(url) + new_headers = self.custom_header_result_queue.get(timeout=5) + headers.update(new_headers) + + if self.config_shortcuts and os.path.isfile(self.config_shortcuts): + domain, _ = parse_url(url) + self.shortcuts_queue.put(domain) + shortcut_url = self.shortcuts_result_queue.get(timeout=5) + if shortcut_url: + response = ( + f"HTTP/1.1 302 Found\r\n" + f"Location: {shortcut_url}\r\n" + f"Content-Length: 0\r\n" + "\r\n" + ) + + client_socket.sendall(response.encode()) + client_socket.close() + self.active_connections.pop(threading.get_ident(), None) + return + + if not self.filter_config.no_filter: + self.filter_queue.put(url) + result = self.filter_result_queue.get(timeout=5) + if result[1] == "Blocked": + if not self.logger_config.no_logging_block: + self.logger_config.block_logger.info( + "%s - %s - %s", + client_socket.getpeername()[0], + url, + first_line + ) + with open(self.html_403, "r", encoding='utf-8') as f: + custom_403_page = f.read() + response = ( + f"HTTP/1.1 403 Forbidden\r\n" + f"Content-Length: {len(custom_403_page)}\r\n" + f"\r\n" + f"{custom_403_page}" + ) + client_socket.sendall(response.encode()) + client_socket.close() + self.active_connections.pop(threading.get_ident(), None) + return + server_host, _ = parse_url(url) + if not self.logger_config.no_logging_access: + self.logger_config.access_logger.info( + "%s - %s - %s", + client_socket.getpeername()[0], + f"http://{server_host}", + first_line + ) + + if self.config_custom_header and os.path.isfile(self.config_custom_header): + request_lines = request.decode(errors='ignore').split("\r\n") + request_line = request_lines[0] # GET / HTTP/1.1 + + header_lines = [f"{key}: {value}" for key, value in headers.items()] + reconstructed_headers = "\r\n".join(header_lines) + + if "\r\n\r\n" in request.decode(errors='ignore'): + body = request.decode(errors='ignore').split("\r\n\r\n", 1)[1] + else: + body = "" + + modified_request = f"{request_line}\r\n{reconstructed_headers}\r\n\r\n{body}".encode() + + self.forward_request_to_server(client_socket, modified_request, url) + + else: + self.forward_request_to_server(client_socket, request, url) + + def forward_request_to_server(self, client_socket, request, url): + """ + Forwards the HTTP request to the target server and sends the response back to the client. + + Args: + client_socket (socket): The socket object for the client connection. + request (bytes): The raw HTTP request sent by the client. + url (str): The target URL from the HTTP request. + """ + if self.proxy_enable: + server_host, server_port = self.proxy_host, self.proxy_port + else: + server_host, server_port = parse_url(url) + thread_id = threading.get_ident() + + if thread_id in self.active_connections: + self.active_connections[thread_id]["target_ip"] = server_host + self.active_connections[thread_id]["target_port"] = server_port + + try: + server_socket = socket.socket(socket.AF_INET, socket.SOCK_STREAM) + server_socket.connect((server_host, server_port)) + server_socket.sendall(request) + server_socket.settimeout(5) + self.active_connections[thread_id]["bytes_sent"] += len(request) + + while True: + try: + response = server_socket.recv(4096) + if response: + client_socket.send(response) + self.active_connections[thread_id]["bytes_received"] += len(response) + else: + break + except socket.timeout: + break + except (socket.timeout, socket.gaierror, ConnectionRefusedError, OSError) as e: + self.console_logger.error("Error connecting to the server %s : %s", server_host, e) + response = ( + f"HTTP/1.1 502 Bad Gateway\r\n" + f"Content-Length: {len('Bad Gateway')} \r\n" + "\r\n" + f"Bad Gateway" + ) + client_socket.sendall(response.encode()) + client_socket.close() + self.active_connections.pop(thread_id, None) + finally: + client_socket.close() + server_socket.close() + self.active_connections.pop(thread_id, None) diff --git a/pyproxy/handlers.py b/pyproxy/handlers/https.py similarity index 58% rename from pyproxy/handlers.py rename to pyproxy/handlers/https.py index 02103ed..ffdb1f8 100644 --- a/pyproxy/handlers.py +++ b/pyproxy/handlers/https.py @@ -1,9 +1,9 @@ """ -handlers.py +pyproxy.handlers.https.py -This module defines the ProxyHandlers class used by the proxy server to process -HTTP and HTTPS client connections. It handles request forwarding, blocking, shortcut -redirection, custom headers, and optional SSL inspection. +This class handles HTTPS CONNECT requests, applies filtering rules, supports SSL inspection, +generates certificates dynamically, and logs access and blocked attempts. It can also +relay raw data when SSL inspection is disabled. """ import socket @@ -13,20 +13,21 @@ import threading from pyproxy.utils.crypto import generate_certificate -from pyproxy.utils.http_req import extract_headers, parse_url # pylint: disable=R0914 -class ProxyHandlers: +class HttpsHandler: """ - ProxyHandlers manages client connections for a proxy server, - handling both HTTP and HTTPS requests. It processes request forwarding, - blocking, SSL inspection, and custom headers based on configuration settings. + Handles HTTPS client connections for a proxy server. + + Supports SSL interception, filtering of targets, and custom logging. This handler + processes HTTPS `CONNECT` requests and either tunnels them directly to the destination + or performs SSL interception for inspection and filtering. """ def __init__(self, html_403, logger_config, filter_config, ssl_config, filter_queue, filter_result_queue, shortcuts_queue, shortcuts_result_queue, cancel_inspect_queue, cancel_inspect_result_queue, custom_header_queue, custom_header_result_queue, console_logger, shortcuts, custom_header, - active_connections): + active_connections, proxy_enable, proxy_host, proxy_port): self.html_403 = html_403 self.logger_config = logger_config self.filter_config = filter_config @@ -42,165 +43,11 @@ def __init__(self, html_403, logger_config, filter_config, ssl_config, self.console_logger = console_logger self.config_shortcuts = shortcuts self.config_custom_header = custom_header + self.proxy_enable=proxy_enable + self.proxy_host=proxy_host + self.proxy_port=proxy_port self.active_connections = active_connections - def handle_client(self, client_socket): - """ - Handles an incoming client connection by processing the request. - - Args: - client_socket (socket): The socket object for the client connection. - """ - request = client_socket.recv(4096) - - 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 first_line.startswith("CONNECT"): - self.handle_https_connection(client_socket, first_line) - else: - self.handle_http_request(client_socket, request) - - def handle_http_request(self, client_socket, request): - """ - Handles HTTP requests, checks if the URL is blocked, - and forwards the request to the target server. - - Args: - client_socket (socket): The socket object for the client connection. - request (bytes): The raw HTTP request sent by the client. - """ - first_line = request.decode(errors='ignore').split("\n")[0] - url = first_line.split(" ")[1] - - if self.config_custom_header and os.path.isfile(self.config_custom_header): - headers = extract_headers(request.decode(errors='ignore')) - self.custom_header_queue.put(url) - new_headers = self.custom_header_result_queue.get() - headers.update(new_headers) - - if self.config_shortcuts and os.path.isfile(self.config_shortcuts): - domain, _ = parse_url(url) - self.shortcuts_queue.put(domain) - shortcut_url = self.shortcuts_result_queue.get() - if shortcut_url: - response = ( - f"HTTP/1.1 302 Found\r\n" - f"Location: {shortcut_url}\r\n" - f"Content-Length: 0\r\n" - "\r\n" - ) - - client_socket.sendall(response.encode()) - client_socket.close() - self.active_connections.pop(threading.get_ident(), None) - return - - if not self.filter_config.no_filter: - self.filter_queue.put(url) - result = self.filter_result_queue.get() - if result[1] == "Blocked": - if not self.logger_config.no_logging_block: - self.logger_config.block_logger.info( - "%s - %s - %s", - client_socket.getpeername()[0], - url, - first_line - ) - with open(self.html_403, "r", encoding='utf-8') as f: - custom_403_page = f.read() - response = ( - f"HTTP/1.1 403 Forbidden\r\n" - f"Content-Length: {len(custom_403_page)}\r\n" - f"\r\n" - f"{custom_403_page}" - ) - client_socket.sendall(response.encode()) - client_socket.close() - self.active_connections.pop(threading.get_ident(), None) - return - server_host, _ = parse_url(url) - if not self.logger_config.no_logging_access: - self.logger_config.access_logger.info( - "%s - %s - %s", - client_socket.getpeername()[0], - f"http://{server_host}", - first_line - ) - - if self.config_custom_header and os.path.isfile(self.config_custom_header): - request_lines = request.decode(errors='ignore').split("\r\n") - request_line = request_lines[0] # GET / HTTP/1.1 - - header_lines = [f"{key}: {value}" for key, value in headers.items()] - reconstructed_headers = "\r\n".join(header_lines) - - if "\r\n\r\n" in request.decode(errors='ignore'): - body = request.decode(errors='ignore').split("\r\n\r\n", 1)[1] - else: - body = "" - - modified_request = f"{request_line}\r\n{reconstructed_headers}\r\n\r\n{body}".encode() - - self.forward_request_to_server(client_socket, modified_request, url) - - else: - self.forward_request_to_server(client_socket, request, url) - - def forward_request_to_server(self, client_socket, request, url): - """ - Forwards the HTTP request to the target server and sends the response back to the client. - - Args: - client_socket (socket): The socket object for the client connection. - request (bytes): The raw HTTP request sent by the client. - url (str): The target URL from the HTTP request. - """ - server_host, server_port = parse_url(url) - thread_id = threading.get_ident() - - if thread_id in self.active_connections: - self.active_connections[thread_id]["target_ip"] = server_host - self.active_connections[thread_id]["target_port"] = server_port - - try: - server_socket = socket.socket(socket.AF_INET, socket.SOCK_STREAM) - server_socket.connect((server_host, server_port)) - server_socket.sendall(request) - server_socket.settimeout(5) - self.active_connections[thread_id]["bytes_sent"] += len(request) - - while True: - try: - response = server_socket.recv(4096) - if response: - client_socket.send(response) - self.active_connections[thread_id]["bytes_received"] += len(response) - else: - break - except socket.timeout: - break - except (socket.timeout, socket.gaierror, ConnectionRefusedError, OSError) as e: - self.console_logger.error("Error connecting to the server %s : %s", server_host, e) - response = ( - f"HTTP/1.1 502 Bad Gateway\r\n" - f"Content-Length: {len('Bad Gateway')} \r\n" - "\r\n" - f"Bad Gateway" - ) - client_socket.sendall(response.encode()) - client_socket.close() - self.active_connections.pop(thread_id, None) - finally: - client_socket.close() - server_socket.close() - self.active_connections.pop(thread_id, None) - # pylint: disable=too-many-locals,too-many-statements,too-many-branches,too-many-nested-blocks def handle_https_connection(self, client_socket, first_line): """ @@ -217,7 +64,7 @@ def handle_https_connection(self, client_socket, first_line): if not self.filter_config.no_filter: self.filter_queue.put(target) - result = self.filter_result_queue.get() + result = self.filter_result_queue.get(timeout=5) if result[1] == "Blocked": if not self.logger_config.no_logging_block: self.logger_config.block_logger.info( @@ -246,7 +93,7 @@ def handle_https_connection(self, client_socket, first_line): and os.path.isfile(self.ssl_config.cancel_inspect) ): self.cancel_inspect_queue.put(server_host) - not_inspect = self.cancel_inspect_result_queue.get() + not_inspect = self.cancel_inspect_result_queue.get(timeout=5) if self.ssl_config.ssl_inspect and not not_inspect: cert_path, key_path = generate_certificate( @@ -272,10 +119,34 @@ def handle_https_connection(self, client_socket, first_line): ) ssl_client_socket.do_handshake() - server_socket = socket.create_connection((server_host, server_port)) + if self.proxy_enable: + next_proxy_socket = socket.create_connection((self.proxy_host, self.proxy_port)) + connect_command = ( + f"CONNECT {server_host}:{server_port} HTTP/1.1\r\n" + f"Host: {server_host}:{server_port}\r\n\r\n" + ) + next_proxy_socket.sendall(connect_command.encode()) + + response = b"" + while b"\r\n\r\n" not in response: + chunk = next_proxy_socket.recv(4096) + if not chunk: + raise ConnectionError("Connection to next proxy failed") + response += chunk + + if b"200 Connection Established" not in response: + raise ConnectionRefusedError("Next proxy refused CONNECT") + + server_socket = next_proxy_socket + else: + server_socket = socket.create_connection((server_host, server_port)) server_context = ssl.SSLContext(ssl.PROTOCOL_TLS_CLIENT) - server_context.load_default_certs() + if self.proxy_enable: + server_context.check_hostname = False + server_context.verify_mode = ssl.CERT_NONE + else: + server_context.load_default_certs() ssl_server_socket = server_context.wrap_socket( server_socket, @@ -292,7 +163,7 @@ def handle_https_connection(self, client_socket, first_line): if not self.filter_config.no_filter: self.filter_queue.put(f"{server_host}{path}") - result = self.filter_result_queue.get() + result = self.filter_result_queue.get(timeout=5) if result[1] == "Blocked": if not self.logger_config.no_logging_block: self.logger_config.block_logger.info( diff --git a/pyproxy/server.py b/pyproxy/server.py index 35e8c1a..df1c9c5 100644 --- a/pyproxy/server.py +++ b/pyproxy/server.py @@ -16,7 +16,7 @@ from pyproxy.utils.version import __slim__ from pyproxy.utils.logger import configure_file_logger, configure_console_logger -from pyproxy.handlers import ProxyHandlers +from pyproxy.handlers.client import ProxyHandlers from pyproxy.modules.filter import filter_process from pyproxy.modules.cancel_inspect import cancel_inspect_process if not __slim__: @@ -43,7 +43,7 @@ class ProxyServer: def __init__(self, host, port, debug, logger_config, filter_config, html_403, ssl_config, shortcuts, custom_header, - flask_port, flask_pass): + flask_port, flask_pass, proxy_enable, proxy_host, proxy_port): """ Initialize the ProxyServer with configuration parameters. """ @@ -60,6 +60,11 @@ def __init__(self, host, port, debug, logger_config, filter_config, self.flask_port = flask_port self.flask_pass = flask_pass + # Proxy + self.proxy_enable=proxy_enable + self.proxy_host=proxy_host + self.proxy_port=proxy_port + # Process communication queues self.filter_proc = None self.filter_queue = multiprocessing.Queue() @@ -231,6 +236,9 @@ def start(self): console_logger=self.console_logger, shortcuts=self.config_shortcuts, custom_header=self.config_custom_header, + proxy_enable=self.proxy_enable, + proxy_host=self.proxy_host, + proxy_port=self.proxy_port, active_connections=self.active_connections ) client_handler = threading.Thread( diff --git a/pyproxy/utils/args.py b/pyproxy/utils/args.py index fe28932..28e2a58 100644 --- a/pyproxy/utils/args.py +++ b/pyproxy/utils/args.py @@ -49,6 +49,9 @@ def parse_args() -> argparse.Namespace: parser.add_argument("--cancel-inspect", type=str, help="Path to the text file containing the list of URLs without ssl inspection") parser.add_argument("--flask-port", type=int, help="Port to listen on for monitoring interface") parser.add_argument("--flask-pass", type=int, help="Default password to Flask interface") + parser.add_argument("--proxy-enable", action="store_true", help="Enable proxy after PyProxy") + parser.add_argument("--proxy-host", type=str, help="Proxy IP to use after PyProxy") + parser.add_argument("--proxy-port", type=int, help="Proxy Port to use after PyProxy") return parser.parse_args() diff --git a/pyproxy/utils/version.py b/pyproxy/utils/version.py index 8112d65..27df6cb 100644 --- a/pyproxy/utils/version.py +++ b/pyproxy/utils/version.py @@ -7,7 +7,7 @@ import os -__version__ = "0.3.1" +__version__ = "0.3.2" if os.path.isdir("pyproxy/monitoring"): __slim__ = False